@aaronsb/kg-cli 0.6.1

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 (266) hide show
  1. package/README.md +112 -0
  2. package/dist/api/client.d.ts +867 -0
  3. package/dist/api/client.d.ts.map +1 -0
  4. package/dist/api/client.js +1362 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/cli/admin/backup.d.ts +9 -0
  7. package/dist/cli/admin/backup.d.ts.map +1 -0
  8. package/dist/cli/admin/backup.js +363 -0
  9. package/dist/cli/admin/backup.js.map +1 -0
  10. package/dist/cli/admin/index.d.ts +7 -0
  11. package/dist/cli/admin/index.d.ts.map +1 -0
  12. package/dist/cli/admin/index.js +52 -0
  13. package/dist/cli/admin/index.js.map +1 -0
  14. package/dist/cli/admin/scheduler.d.ts +7 -0
  15. package/dist/cli/admin/scheduler.d.ts.map +1 -0
  16. package/dist/cli/admin/scheduler.js +125 -0
  17. package/dist/cli/admin/scheduler.js.map +1 -0
  18. package/dist/cli/admin/status.d.ts +7 -0
  19. package/dist/cli/admin/status.d.ts.map +1 -0
  20. package/dist/cli/admin/status.js +134 -0
  21. package/dist/cli/admin/status.js.map +1 -0
  22. package/dist/cli/admin/utils.d.ts +34 -0
  23. package/dist/cli/admin/utils.d.ts.map +1 -0
  24. package/dist/cli/admin/utils.js +441 -0
  25. package/dist/cli/admin/utils.js.map +1 -0
  26. package/dist/cli/ai-config/embedding.d.ts +11 -0
  27. package/dist/cli/ai-config/embedding.d.ts.map +1 -0
  28. package/dist/cli/ai-config/embedding.js +598 -0
  29. package/dist/cli/ai-config/embedding.js.map +1 -0
  30. package/dist/cli/ai-config/extraction.d.ts +11 -0
  31. package/dist/cli/ai-config/extraction.d.ts.map +1 -0
  32. package/dist/cli/ai-config/extraction.js +206 -0
  33. package/dist/cli/ai-config/extraction.js.map +1 -0
  34. package/dist/cli/ai-config/index.d.ts +21 -0
  35. package/dist/cli/ai-config/index.d.ts.map +1 -0
  36. package/dist/cli/ai-config/index.js +27 -0
  37. package/dist/cli/ai-config/index.js.map +1 -0
  38. package/dist/cli/ai-config/keys.d.ts +11 -0
  39. package/dist/cli/ai-config/keys.d.ts.map +1 -0
  40. package/dist/cli/ai-config/keys.js +182 -0
  41. package/dist/cli/ai-config/keys.js.map +1 -0
  42. package/dist/cli/ai-config/utils.d.ts +13 -0
  43. package/dist/cli/ai-config/utils.d.ts.map +1 -0
  44. package/dist/cli/ai-config/utils.js +84 -0
  45. package/dist/cli/ai-config/utils.js.map +1 -0
  46. package/dist/cli/artifact.d.ts +8 -0
  47. package/dist/cli/artifact.d.ts.map +1 -0
  48. package/dist/cli/artifact.js +296 -0
  49. package/dist/cli/artifact.js.map +1 -0
  50. package/dist/cli/auth-admin.d.ts +11 -0
  51. package/dist/cli/auth-admin.d.ts.map +1 -0
  52. package/dist/cli/auth-admin.js +415 -0
  53. package/dist/cli/auth-admin.js.map +1 -0
  54. package/dist/cli/colors.d.ts +105 -0
  55. package/dist/cli/colors.d.ts.map +1 -0
  56. package/dist/cli/colors.js +164 -0
  57. package/dist/cli/colors.js.map +1 -0
  58. package/dist/cli/commands.d.ts +6 -0
  59. package/dist/cli/commands.d.ts.map +1 -0
  60. package/dist/cli/commands.js +164 -0
  61. package/dist/cli/commands.js.map +1 -0
  62. package/dist/cli/config.d.ts +6 -0
  63. package/dist/cli/config.d.ts.map +1 -0
  64. package/dist/cli/config.js +694 -0
  65. package/dist/cli/config.js.map +1 -0
  66. package/dist/cli/curve-viz.d.ts +89 -0
  67. package/dist/cli/curve-viz.d.ts.map +1 -0
  68. package/dist/cli/curve-viz.js +228 -0
  69. package/dist/cli/curve-viz.js.map +1 -0
  70. package/dist/cli/database.d.ts +6 -0
  71. package/dist/cli/database.d.ts.map +1 -0
  72. package/dist/cli/database.js +324 -0
  73. package/dist/cli/database.js.map +1 -0
  74. package/dist/cli/document.d.ts +6 -0
  75. package/dist/cli/document.d.ts.map +1 -0
  76. package/dist/cli/document.js +458 -0
  77. package/dist/cli/document.js.map +1 -0
  78. package/dist/cli/group.d.ts +8 -0
  79. package/dist/cli/group.d.ts.map +1 -0
  80. package/dist/cli/group.js +174 -0
  81. package/dist/cli/group.js.map +1 -0
  82. package/dist/cli/health.d.ts +6 -0
  83. package/dist/cli/health.d.ts.map +1 -0
  84. package/dist/cli/health.js +34 -0
  85. package/dist/cli/health.js.map +1 -0
  86. package/dist/cli/help-formatter.d.ts +16 -0
  87. package/dist/cli/help-formatter.d.ts.map +1 -0
  88. package/dist/cli/help-formatter.js +248 -0
  89. package/dist/cli/help-formatter.js.map +1 -0
  90. package/dist/cli/help.d.ts +9 -0
  91. package/dist/cli/help.d.ts.map +1 -0
  92. package/dist/cli/help.js +227 -0
  93. package/dist/cli/help.js.map +1 -0
  94. package/dist/cli/ingest.d.ts +6 -0
  95. package/dist/cli/ingest.d.ts.map +1 -0
  96. package/dist/cli/ingest.js +722 -0
  97. package/dist/cli/ingest.js.map +1 -0
  98. package/dist/cli/jobs.d.ts +6 -0
  99. package/dist/cli/jobs.d.ts.map +1 -0
  100. package/dist/cli/jobs.js +663 -0
  101. package/dist/cli/jobs.js.map +1 -0
  102. package/dist/cli/login.d.ts +21 -0
  103. package/dist/cli/login.d.ts.map +1 -0
  104. package/dist/cli/login.js +221 -0
  105. package/dist/cli/login.js.map +1 -0
  106. package/dist/cli/logout.d.ts +16 -0
  107. package/dist/cli/logout.d.ts.map +1 -0
  108. package/dist/cli/logout.js +141 -0
  109. package/dist/cli/logout.js.map +1 -0
  110. package/dist/cli/mcp-config.d.ts +10 -0
  111. package/dist/cli/mcp-config.d.ts.map +1 -0
  112. package/dist/cli/mcp-config.js +358 -0
  113. package/dist/cli/mcp-config.js.map +1 -0
  114. package/dist/cli/oauth.d.ts +15 -0
  115. package/dist/cli/oauth.d.ts.map +1 -0
  116. package/dist/cli/oauth.js +296 -0
  117. package/dist/cli/oauth.js.map +1 -0
  118. package/dist/cli/ontology.d.ts +6 -0
  119. package/dist/cli/ontology.d.ts.map +1 -0
  120. package/dist/cli/ontology.js +231 -0
  121. package/dist/cli/ontology.js.map +1 -0
  122. package/dist/cli/polarity.d.ts +6 -0
  123. package/dist/cli/polarity.d.ts.map +1 -0
  124. package/dist/cli/polarity.js +295 -0
  125. package/dist/cli/polarity.js.map +1 -0
  126. package/dist/cli/projection.d.ts +8 -0
  127. package/dist/cli/projection.d.ts.map +1 -0
  128. package/dist/cli/projection.js +297 -0
  129. package/dist/cli/projection.js.map +1 -0
  130. package/dist/cli/query-def.d.ts +8 -0
  131. package/dist/cli/query-def.d.ts.map +1 -0
  132. package/dist/cli/query-def.js +163 -0
  133. package/dist/cli/query-def.js.map +1 -0
  134. package/dist/cli/rbac.d.ts +12 -0
  135. package/dist/cli/rbac.d.ts.map +1 -0
  136. package/dist/cli/rbac.js +615 -0
  137. package/dist/cli/rbac.js.map +1 -0
  138. package/dist/cli/search.d.ts +6 -0
  139. package/dist/cli/search.d.ts.map +1 -0
  140. package/dist/cli/search.js +829 -0
  141. package/dist/cli/search.js.map +1 -0
  142. package/dist/cli/source.d.ts +6 -0
  143. package/dist/cli/source.d.ts.map +1 -0
  144. package/dist/cli/source.js +202 -0
  145. package/dist/cli/source.js.map +1 -0
  146. package/dist/cli/verb-router.d.ts +25 -0
  147. package/dist/cli/verb-router.d.ts.map +1 -0
  148. package/dist/cli/verb-router.js +415 -0
  149. package/dist/cli/verb-router.js.map +1 -0
  150. package/dist/cli/vocabulary/config.d.ts +7 -0
  151. package/dist/cli/vocabulary/config.d.ts.map +1 -0
  152. package/dist/cli/vocabulary/config.js +201 -0
  153. package/dist/cli/vocabulary/config.js.map +1 -0
  154. package/dist/cli/vocabulary/consolidate.d.ts +8 -0
  155. package/dist/cli/vocabulary/consolidate.d.ts.map +1 -0
  156. package/dist/cli/vocabulary/consolidate.js +192 -0
  157. package/dist/cli/vocabulary/consolidate.js.map +1 -0
  158. package/dist/cli/vocabulary/embeddings.d.ts +9 -0
  159. package/dist/cli/vocabulary/embeddings.d.ts.map +1 -0
  160. package/dist/cli/vocabulary/embeddings.js +205 -0
  161. package/dist/cli/vocabulary/embeddings.js.map +1 -0
  162. package/dist/cli/vocabulary/epistemic.d.ts +7 -0
  163. package/dist/cli/vocabulary/epistemic.d.ts.map +1 -0
  164. package/dist/cli/vocabulary/epistemic.js +315 -0
  165. package/dist/cli/vocabulary/epistemic.js.map +1 -0
  166. package/dist/cli/vocabulary/index.d.ts +7 -0
  167. package/dist/cli/vocabulary/index.d.ts.map +1 -0
  168. package/dist/cli/vocabulary/index.js +45 -0
  169. package/dist/cli/vocabulary/index.js.map +1 -0
  170. package/dist/cli/vocabulary/profiles.d.ts +7 -0
  171. package/dist/cli/vocabulary/profiles.d.ts.map +1 -0
  172. package/dist/cli/vocabulary/profiles.js +171 -0
  173. package/dist/cli/vocabulary/profiles.js.map +1 -0
  174. package/dist/cli/vocabulary/similarity.d.ts +9 -0
  175. package/dist/cli/vocabulary/similarity.d.ts.map +1 -0
  176. package/dist/cli/vocabulary/similarity.js +199 -0
  177. package/dist/cli/vocabulary/similarity.js.map +1 -0
  178. package/dist/cli/vocabulary/status.d.ts +8 -0
  179. package/dist/cli/vocabulary/status.d.ts.map +1 -0
  180. package/dist/cli/vocabulary/status.js +280 -0
  181. package/dist/cli/vocabulary/status.js.map +1 -0
  182. package/dist/cli/vocabulary/sync.d.ts +7 -0
  183. package/dist/cli/vocabulary/sync.d.ts.map +1 -0
  184. package/dist/cli/vocabulary/sync.js +111 -0
  185. package/dist/cli/vocabulary/sync.js.map +1 -0
  186. package/dist/index.d.ts +9 -0
  187. package/dist/index.d.ts.map +1 -0
  188. package/dist/index.js +16 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/lib/auth/auth-client.d.ts +247 -0
  191. package/dist/lib/auth/auth-client.d.ts.map +1 -0
  192. package/dist/lib/auth/auth-client.js +305 -0
  193. package/dist/lib/auth/auth-client.js.map +1 -0
  194. package/dist/lib/auth/challenge.d.ts +39 -0
  195. package/dist/lib/auth/challenge.d.ts.map +1 -0
  196. package/dist/lib/auth/challenge.js +125 -0
  197. package/dist/lib/auth/challenge.js.map +1 -0
  198. package/dist/lib/auth/client-credentials-flow.d.ts +58 -0
  199. package/dist/lib/auth/client-credentials-flow.d.ts.map +1 -0
  200. package/dist/lib/auth/client-credentials-flow.js +118 -0
  201. package/dist/lib/auth/client-credentials-flow.js.map +1 -0
  202. package/dist/lib/auth/device-flow.d.ts +75 -0
  203. package/dist/lib/auth/device-flow.d.ts.map +1 -0
  204. package/dist/lib/auth/device-flow.js +177 -0
  205. package/dist/lib/auth/device-flow.js.map +1 -0
  206. package/dist/lib/auth/index.d.ts +14 -0
  207. package/dist/lib/auth/index.d.ts.map +1 -0
  208. package/dist/lib/auth/index.js +34 -0
  209. package/dist/lib/auth/index.js.map +1 -0
  210. package/dist/lib/auth/oauth-types.d.ts +69 -0
  211. package/dist/lib/auth/oauth-types.d.ts.map +1 -0
  212. package/dist/lib/auth/oauth-types.js +10 -0
  213. package/dist/lib/auth/oauth-types.js.map +1 -0
  214. package/dist/lib/auth/oauth-utils.d.ts +51 -0
  215. package/dist/lib/auth/oauth-utils.d.ts.map +1 -0
  216. package/dist/lib/auth/oauth-utils.js +110 -0
  217. package/dist/lib/auth/oauth-utils.js.map +1 -0
  218. package/dist/lib/auth/token-manager.d.ts +87 -0
  219. package/dist/lib/auth/token-manager.d.ts.map +1 -0
  220. package/dist/lib/auth/token-manager.js +139 -0
  221. package/dist/lib/auth/token-manager.js.map +1 -0
  222. package/dist/lib/auth/token-refresh.d.ts +63 -0
  223. package/dist/lib/auth/token-refresh.d.ts.map +1 -0
  224. package/dist/lib/auth/token-refresh.js +141 -0
  225. package/dist/lib/auth/token-refresh.js.map +1 -0
  226. package/dist/lib/config.d.ts +286 -0
  227. package/dist/lib/config.d.ts.map +1 -0
  228. package/dist/lib/config.js +537 -0
  229. package/dist/lib/config.js.map +1 -0
  230. package/dist/lib/job-stream.d.ts +53 -0
  231. package/dist/lib/job-stream.d.ts.map +1 -0
  232. package/dist/lib/job-stream.js +153 -0
  233. package/dist/lib/job-stream.js.map +1 -0
  234. package/dist/lib/mcp-allowlist.d.ts +101 -0
  235. package/dist/lib/mcp-allowlist.d.ts.map +1 -0
  236. package/dist/lib/mcp-allowlist.js +340 -0
  237. package/dist/lib/mcp-allowlist.js.map +1 -0
  238. package/dist/lib/table-example.d.ts +7 -0
  239. package/dist/lib/table-example.d.ts.map +1 -0
  240. package/dist/lib/table-example.js +105 -0
  241. package/dist/lib/table-example.js.map +1 -0
  242. package/dist/lib/table.d.ts +95 -0
  243. package/dist/lib/table.d.ts.map +1 -0
  244. package/dist/lib/table.js +263 -0
  245. package/dist/lib/table.js.map +1 -0
  246. package/dist/lib/terminal-images.d.ts +66 -0
  247. package/dist/lib/terminal-images.d.ts.map +1 -0
  248. package/dist/lib/terminal-images.js +268 -0
  249. package/dist/lib/terminal-images.js.map +1 -0
  250. package/dist/mcp/formatters.d.ts +100 -0
  251. package/dist/mcp/formatters.d.ts.map +1 -0
  252. package/dist/mcp/formatters.js +1411 -0
  253. package/dist/mcp/formatters.js.map +1 -0
  254. package/dist/mcp-server.d.ts +9 -0
  255. package/dist/mcp-server.d.ts.map +1 -0
  256. package/dist/mcp-server.js +1810 -0
  257. package/dist/mcp-server.js.map +1 -0
  258. package/dist/types/index.d.ts +742 -0
  259. package/dist/types/index.d.ts.map +1 -0
  260. package/dist/types/index.js +6 -0
  261. package/dist/types/index.js.map +1 -0
  262. package/dist/version.d.ts +10 -0
  263. package/dist/version.d.ts.map +1 -0
  264. package/dist/version.js +13 -0
  265. package/dist/version.js.map +1 -0
  266. package/package.json +84 -0
@@ -0,0 +1,1810 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Knowledge Graph MCP Server
5
+ *
6
+ * Standalone MCP server exposing knowledge graph operations as MCP tools.
7
+ * Reuses the existing API client and types from the CLI.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
44
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
45
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
46
+ const client_js_1 = require("./api/client.js");
47
+ const auth_client_js_1 = require("./lib/auth/auth-client.js");
48
+ const mcp_allowlist_js_1 = require("./lib/mcp-allowlist.js");
49
+ const formatters_js_1 = require("./mcp/formatters.js");
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ /**
53
+ * Default parameters for graph queries (ADR-048 Query Safety)
54
+ *
55
+ * These defaults balance performance and result quality:
56
+ * - Higher thresholds (0.75+) prevent expensive full-graph scans
57
+ * - Lower max_hops (3) prevent exponential traversal explosion
58
+ * - Adjust based on graph size and performance characteristics
59
+ */
60
+ const DEFAULT_SEARCH_SIMILARITY = 0.7; // Search tool minimum similarity
61
+ const DEFAULT_SEMANTIC_THRESHOLD = 0.75; // Connect queries semantic matching
62
+ const DEFAULT_MAX_HOPS = 3; // Maximum path traversal depth
63
+ const DEFAULT_MAX_DEPTH = 2; // Related concepts neighborhood depth
64
+ // Create server instance
65
+ const server = new index_js_1.Server({
66
+ name: 'knowledge-graph-server',
67
+ version: '0.1.0',
68
+ }, {
69
+ capabilities: {
70
+ tools: {},
71
+ prompts: {},
72
+ resources: {},
73
+ },
74
+ });
75
+ /**
76
+ * Knowledge Graph Server - Exploration Guide
77
+ *
78
+ * This system transforms documents into semantic concept graphs with multi-dimensional scoring:
79
+ * - Grounding strength (-1.0 to 1.0): Reliability/contradiction score
80
+ * - Diversity score (0-100%): Conceptual richness and connection breadth
81
+ * - Authenticated diversity (✅✓⚠❌): Directional quality combining grounding + diversity
82
+ *
83
+ * Explore by:
84
+ * 1. search - Find entry points (returns all scores + evidence samples)
85
+ * 2. concept - Work with concepts (details, related, connections)
86
+ * 3. ontology - Manage ontologies (list, info, files, delete)
87
+ * 4. job - Manage jobs (status, list, approve, cancel)
88
+ * 5. ingest - Ingest content (text, inspect-file, file, directory)
89
+ * 6. source - Retrieve source images for visual verification
90
+ *
91
+ * Resources provide fresh data on-demand without consuming tool budget:
92
+ * - database/stats, database/info, database/health
93
+ * - system/status, api/health
94
+ *
95
+ * Use high grounding + high diversity to find reliable, central concepts.
96
+ * Negative grounding often shows the most interesting problems/contradictions.
97
+ */
98
+ // OAuth access token storage for authenticated session (ADR-054)
99
+ let oauthAccessToken = null;
100
+ let tokenRefreshTimer = null;
101
+ /**
102
+ * Get OAuth access token using client credentials grant (ADR-054)
103
+ * Returns the token and expiry time in milliseconds
104
+ */
105
+ async function getOAuthAccessToken() {
106
+ const clientId = process.env.KG_OAUTH_CLIENT_ID;
107
+ const clientSecret = process.env.KG_OAUTH_CLIENT_SECRET;
108
+ const apiUrl = process.env.KG_API_URL || 'http://localhost:8000';
109
+ if (!clientId || !clientSecret) {
110
+ console.error('[MCP Auth] Missing OAuth credentials: KG_OAUTH_CLIENT_ID and KG_OAUTH_CLIENT_SECRET required');
111
+ return null;
112
+ }
113
+ try {
114
+ const authClient = new auth_client_js_1.AuthClient(apiUrl);
115
+ const tokenResponse = await authClient.getOAuthToken({
116
+ grant_type: 'client_credentials',
117
+ client_id: clientId,
118
+ client_secret: clientSecret,
119
+ scope: 'read:* write:*'
120
+ });
121
+ const expiresInMs = tokenResponse.expires_in * 1000;
122
+ const expiryTime = new Date(Date.now() + expiresInMs);
123
+ console.error(`[MCP Auth] Successfully authenticated with OAuth client`);
124
+ console.error(`[MCP Auth] Client ID: ${clientId}`);
125
+ console.error(`[MCP Auth] Token expires at ${expiryTime.toISOString()}`);
126
+ return {
127
+ token: tokenResponse.access_token,
128
+ expiresInMs
129
+ };
130
+ }
131
+ catch (error) {
132
+ console.error(`[MCP Auth] Failed to get OAuth token: ${error.message}`);
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Schedule automatic token refresh before expiry
138
+ * Refreshes 5 minutes before the token expires
139
+ */
140
+ function scheduleTokenRefresh(expiresInMs) {
141
+ // Clear existing timer if any
142
+ if (tokenRefreshTimer) {
143
+ clearTimeout(tokenRefreshTimer);
144
+ }
145
+ // Refresh 5 minutes (300000ms) before expiry, or halfway through if token life < 10 minutes
146
+ const refreshBeforeMs = Math.min(300000, expiresInMs / 2);
147
+ const refreshInMs = expiresInMs - refreshBeforeMs;
148
+ console.error(`[MCP Auth] Token refresh scheduled in ${Math.round(refreshInMs / 1000 / 60)} minutes`);
149
+ tokenRefreshTimer = setTimeout(async () => {
150
+ console.error('[MCP Auth] Refreshing OAuth access token...');
151
+ const result = await getOAuthAccessToken();
152
+ if (result) {
153
+ oauthAccessToken = result.token;
154
+ // Update the token in the client
155
+ if (client) {
156
+ client.setMcpJwtToken(result.token);
157
+ }
158
+ // Schedule next refresh
159
+ scheduleTokenRefresh(result.expiresInMs);
160
+ }
161
+ else {
162
+ console.error('[MCP Auth] Token refresh failed! Operations may fail with 401 errors.');
163
+ console.error('[MCP Auth] Please restart the MCP server or check OAuth credentials.');
164
+ }
165
+ }, refreshInMs);
166
+ }
167
+ /**
168
+ * Initialize OAuth authentication using client credentials (ADR-054)
169
+ *
170
+ * Requires KG_OAUTH_CLIENT_ID and KG_OAUTH_CLIENT_SECRET environment variables.
171
+ * This allows the MCP server to authenticate transparently without
172
+ * the AI being aware of authentication requirements.
173
+ */
174
+ async function initializeAuth() {
175
+ const clientId = process.env.KG_OAUTH_CLIENT_ID;
176
+ const clientSecret = process.env.KG_OAUTH_CLIENT_SECRET;
177
+ if (clientId && clientSecret) {
178
+ const result = await getOAuthAccessToken();
179
+ if (result) {
180
+ oauthAccessToken = result.token;
181
+ scheduleTokenRefresh(result.expiresInMs);
182
+ }
183
+ else {
184
+ console.error('[MCP Auth] The MCP server will operate without authentication.');
185
+ console.error('[MCP Auth] Some operations may fail with 401 errors.');
186
+ }
187
+ }
188
+ else {
189
+ console.error('[MCP Auth] No OAuth credentials provided (KG_OAUTH_CLIENT_ID/KG_OAUTH_CLIENT_SECRET).');
190
+ console.error('[MCP Auth] The MCP server will operate without authentication.');
191
+ console.error('[MCP Auth] To authenticate:');
192
+ console.error('[MCP Auth] 1. Create a personal OAuth client: kg login');
193
+ console.error('[MCP Auth] 2. Copy client credentials from ~/.config/kg/config.json');
194
+ console.error('[MCP Auth] 3. Add to Claude Desktop config as KG_OAUTH_CLIENT_ID and KG_OAUTH_CLIENT_SECRET');
195
+ }
196
+ }
197
+ /**
198
+ * Create an authenticated API client
199
+ * If we have an OAuth access token, inject it into the client (ADR-054)
200
+ */
201
+ function createAuthenticatedClient() {
202
+ const client = (0, client_js_1.createClientFromEnv)();
203
+ // If we have an OAuth access token, set it on the client
204
+ if (oauthAccessToken) {
205
+ client.setMcpJwtToken(oauthAccessToken);
206
+ }
207
+ return client;
208
+ }
209
+ // Client instance - will be initialized in main() before server starts
210
+ let client;
211
+ /**
212
+ * Tool Definitions
213
+ *
214
+ * Consolidated from 22 tools to 6 tools + 5 resources (ADR-013)
215
+ */
216
+ // List available tools
217
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
218
+ return {
219
+ tools: [
220
+ {
221
+ name: 'search',
222
+ description: `Search for concepts, source passages, or documents using semantic similarity. Your ENTRY POINT to the graph.
223
+
224
+ CONCEPT SEARCH (type: "concepts", default) - Find concepts by semantic similarity:
225
+ - Grounding strength (-1.0 to 1.0): Reliability/contradiction score
226
+ - Diversity score: Conceptual richness (% of diverse connections)
227
+ - Authenticated diversity: Support vs contradiction indicator (✅✓⚠❌)
228
+ - Evidence samples: Quoted text from source documents
229
+ - Image indicators: Visual evidence when available
230
+ - Document sources: Where concepts originated
231
+
232
+ SOURCE SEARCH (type: "sources") - Find source text passages directly (ADR-068):
233
+ - Searches source document embeddings, not concept embeddings
234
+ - Returns matched text chunks with character offsets for highlighting
235
+ - Shows concepts extracted from those passages
236
+ - Useful for RAG workflows and finding original context
237
+
238
+ DOCUMENT SEARCH (type: "documents") - Find documents by semantic similarity (ADR-084):
239
+ - Searches at document level (aggregates source chunks)
240
+ - Returns documents ranked by best matching chunk similarity
241
+ - Shows concepts extracted from each document
242
+ - Use with document tool for content retrieval
243
+
244
+ RECOMMENDED WORKFLOW: After search, use concept (action: "connect") to find HOW concepts relate - this reveals narrative flows and cause/effect chains that individual searches cannot show. Connection paths are often more valuable than isolated concepts.
245
+
246
+ Use 2-3 word phrases (e.g., "linear thinking patterns").`,
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: {
250
+ query: {
251
+ type: 'string',
252
+ description: 'Search query text (2-3 word phrases work best, e.g., "linear thinking patterns")',
253
+ },
254
+ type: {
255
+ type: 'string',
256
+ enum: ['concepts', 'sources', 'documents'],
257
+ description: 'Search type: "concepts" (default), "sources" (passage search), or "documents" (document-level search)',
258
+ default: 'concepts',
259
+ },
260
+ limit: {
261
+ type: 'number',
262
+ description: 'Maximum number of results to return (default: 10, max: 100)',
263
+ default: 10,
264
+ },
265
+ min_similarity: {
266
+ type: 'number',
267
+ description: 'Minimum similarity score 0.0-1.0 (default: 0.7 for 70%, lower to 0.5-0.6 for broader matches)',
268
+ default: 0.7,
269
+ },
270
+ offset: {
271
+ type: 'number',
272
+ description: 'Number of results to skip for pagination (default: 0)',
273
+ default: 0,
274
+ },
275
+ ontology: {
276
+ type: 'string',
277
+ description: 'Filter by ontology/document name (sources only)',
278
+ },
279
+ },
280
+ required: ['query'],
281
+ },
282
+ },
283
+ {
284
+ name: 'concept',
285
+ description: `Work with concepts: get details (ALL evidence + relationships), find related concepts (neighborhood exploration), or discover connections (paths between concepts).
286
+
287
+ PERFORMANCE CRITICAL: For "connect" action, use threshold >= 0.75 to avoid database overload. Lower thresholds create exponentially larger searches that can hang for minutes. Start with threshold=0.8, max_hops=3, then adjust if needed.`,
288
+ inputSchema: {
289
+ type: 'object',
290
+ properties: {
291
+ action: {
292
+ type: 'string',
293
+ enum: ['details', 'related', 'connect'],
294
+ description: 'Operation: "details" (get ALL evidence), "related" (explore neighborhood), "connect" (find paths)',
295
+ },
296
+ // For details and related
297
+ concept_id: {
298
+ type: 'string',
299
+ description: 'Concept ID (required for details, related)',
300
+ },
301
+ include_grounding: {
302
+ type: 'boolean',
303
+ description: 'Include grounding_strength (default: true)',
304
+ default: true,
305
+ },
306
+ include_diversity: {
307
+ type: 'boolean',
308
+ description: 'Include diversity metrics for details action (default: false, adds ~100-500ms)',
309
+ default: false,
310
+ },
311
+ diversity_max_hops: {
312
+ type: 'number',
313
+ description: 'Max hops for diversity calculation (default: 2)',
314
+ default: 2,
315
+ },
316
+ truncate_evidence: {
317
+ type: 'boolean',
318
+ description: 'Truncate evidence full_text context to 200 chars (default: true for token efficiency). Set false for complete context.',
319
+ default: true,
320
+ },
321
+ // For related
322
+ max_depth: {
323
+ type: 'number',
324
+ description: 'Max traversal depth for related (1-5, default: 2)',
325
+ default: 2,
326
+ },
327
+ relationship_types: {
328
+ type: 'array',
329
+ items: { type: 'string' },
330
+ description: 'Filter relationships (e.g., ["SUPPORTS", "CONTRADICTS"])',
331
+ },
332
+ // ADR-065: Epistemic status filtering (for related and connect)
333
+ include_epistemic_status: {
334
+ type: 'array',
335
+ items: { type: 'string' },
336
+ description: 'Only include relationships with these epistemic statuses (e.g., ["AFFIRMATIVE", "CONTESTED"])',
337
+ },
338
+ exclude_epistemic_status: {
339
+ type: 'array',
340
+ items: { type: 'string' },
341
+ description: 'Exclude relationships with these epistemic statuses (e.g., ["HISTORICAL", "INSUFFICIENT_DATA"])',
342
+ },
343
+ // For connect
344
+ connection_mode: {
345
+ type: 'string',
346
+ enum: ['exact', 'semantic'],
347
+ description: 'Connection mode: "exact" (IDs) or "semantic" (phrases)',
348
+ default: 'semantic',
349
+ },
350
+ from_id: {
351
+ type: 'string',
352
+ description: 'Starting concept ID (for exact mode)',
353
+ },
354
+ to_id: {
355
+ type: 'string',
356
+ description: 'Target concept ID (for exact mode)',
357
+ },
358
+ from_query: {
359
+ type: 'string',
360
+ description: 'Starting phrase (for semantic mode, 2-3 words)',
361
+ },
362
+ to_query: {
363
+ type: 'string',
364
+ description: 'Target phrase (for semantic mode, 2-3 words)',
365
+ },
366
+ max_hops: {
367
+ type: 'number',
368
+ description: 'Max path length (default: 3). WARNING: Values >5 combined with threshold <0.75 can cause severe performance issues.',
369
+ default: 3,
370
+ },
371
+ threshold: {
372
+ type: 'number',
373
+ description: 'Similarity threshold for semantic mode (default: 0.75). PERFORMANCE GUIDE: 0.85+ = precise/fast, 0.75-0.84 = balanced, 0.60-0.74 = exploratory/SLOW, <0.60 = DANGEROUS (can hang database for minutes)',
374
+ default: 0.75,
375
+ },
376
+ },
377
+ required: ['action'],
378
+ },
379
+ },
380
+ {
381
+ name: 'ontology',
382
+ description: 'Manage ontologies (knowledge domains/collections): list all, get info, list files, or delete. Use action parameter to specify operation.',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ action: {
387
+ type: 'string',
388
+ enum: ['list', 'info', 'files', 'delete'],
389
+ description: 'Operation: "list" (all ontologies), "info" (details), "files" (source files), "delete" (remove)',
390
+ },
391
+ ontology_name: {
392
+ type: 'string',
393
+ description: 'Ontology name (required for info, files, delete)',
394
+ },
395
+ force: {
396
+ type: 'boolean',
397
+ description: 'Confirm deletion (required for delete)',
398
+ default: false,
399
+ },
400
+ },
401
+ required: ['action'],
402
+ },
403
+ },
404
+ {
405
+ name: 'job',
406
+ description: 'Manage ingestion jobs: get status, list jobs, approve, or cancel. Use action parameter to specify operation.',
407
+ inputSchema: {
408
+ type: 'object',
409
+ properties: {
410
+ action: {
411
+ type: 'string',
412
+ enum: ['status', 'list', 'approve', 'cancel'],
413
+ description: 'Operation: "status" (get job status), "list" (list jobs), "approve" (approve job), "cancel" (cancel job)',
414
+ },
415
+ job_id: {
416
+ type: 'string',
417
+ description: 'Job ID (required for status, approve, cancel)',
418
+ },
419
+ status: {
420
+ type: 'string',
421
+ description: 'Filter by status for list (pending, awaiting_approval, running, completed, failed)',
422
+ },
423
+ limit: {
424
+ type: 'number',
425
+ description: 'Max jobs to return for list (default: 50)',
426
+ default: 50,
427
+ },
428
+ },
429
+ required: ['action'],
430
+ },
431
+ },
432
+ {
433
+ name: 'ingest',
434
+ description: 'Ingest content into the knowledge graph: submit text, inspect files, ingest files, or ingest directories. Use action parameter to specify operation.',
435
+ inputSchema: {
436
+ type: 'object',
437
+ properties: {
438
+ action: {
439
+ type: 'string',
440
+ enum: ['text', 'inspect-file', 'file', 'directory'],
441
+ description: 'Operation: "text" (raw text), "inspect-file" (validate), "file" (ingest files), "directory" (ingest directory)',
442
+ },
443
+ // For text action
444
+ text: {
445
+ type: 'string',
446
+ description: 'Text content to ingest (required for text action)',
447
+ },
448
+ // For all actions except inspect-file
449
+ ontology: {
450
+ type: 'string',
451
+ description: 'Ontology name (required for text/file/directory, optional for directory - defaults to dir name)',
452
+ },
453
+ // For text action
454
+ filename: {
455
+ type: 'string',
456
+ description: 'Optional filename for source tracking (text action)',
457
+ },
458
+ processing_mode: {
459
+ type: 'string',
460
+ enum: ['serial', 'parallel'],
461
+ description: 'Processing mode (text action, default: serial)',
462
+ default: 'serial',
463
+ },
464
+ target_words: {
465
+ type: 'number',
466
+ description: 'Words per chunk (text action, default: 1000)',
467
+ default: 1000,
468
+ },
469
+ overlap_words: {
470
+ type: 'number',
471
+ description: 'Overlap between chunks (text action, default: 200)',
472
+ default: 200,
473
+ },
474
+ // For file/directory/inspect-file actions
475
+ path: {
476
+ description: 'File/directory path (required for inspect-file/file/directory). For file action: single path string OR array for batch',
477
+ oneOf: [
478
+ { type: 'string' },
479
+ { type: 'array', items: { type: 'string' } }
480
+ ],
481
+ },
482
+ // For file/directory actions
483
+ auto_approve: {
484
+ type: 'boolean',
485
+ description: 'Auto-approve processing (file/directory actions, default: true)',
486
+ default: true,
487
+ },
488
+ force: {
489
+ type: 'boolean',
490
+ description: 'Force re-ingestion (file/directory actions, default: false)',
491
+ default: false,
492
+ },
493
+ // For directory action
494
+ recursive: {
495
+ type: 'boolean',
496
+ description: 'Process subdirectories recursively (directory action, default: false)',
497
+ default: false,
498
+ },
499
+ limit: {
500
+ type: 'number',
501
+ description: 'Number of files to show per page (directory action, default: 10)',
502
+ default: 10,
503
+ },
504
+ offset: {
505
+ type: 'number',
506
+ description: 'Number of files to skip for pagination (directory action, default: 0)',
507
+ default: 0,
508
+ },
509
+ },
510
+ required: ['action'],
511
+ },
512
+ },
513
+ {
514
+ name: 'source',
515
+ description: `Retrieve original source content (text or image) for a source node (ADR-057).
516
+
517
+ For IMAGE sources: Returns the image for visual verification
518
+ For TEXT sources: Returns full_text content with metadata (document, paragraph, offsets)
519
+
520
+ Use when you need to:
521
+ - Verify extracted concepts against original source
522
+ - Get the full context of a text passage
523
+ - Retrieve images for visual analysis
524
+ - Check character offsets for highlighting`,
525
+ inputSchema: {
526
+ type: 'object',
527
+ properties: {
528
+ source_id: {
529
+ type: 'string',
530
+ description: 'Source ID from evidence or search results',
531
+ },
532
+ },
533
+ required: ['source_id'],
534
+ },
535
+ },
536
+ {
537
+ name: 'epistemic_status',
538
+ description: `Vocabulary epistemic status classification (ADR-065 Phase 2). Knowledge validation state for relationship types.
539
+
540
+ Three actions available:
541
+ - "list": List all vocabulary types with epistemic status classifications (AFFIRMATIVE/CONTESTED/CONTRADICTORY/HISTORICAL/INSUFFICIENT_DATA/UNCLASSIFIED)
542
+ - "show": Get detailed status for a specific relationship type
543
+ - "measure": Run measurement to calculate epistemic status for all types (admin operation)
544
+
545
+ EPISTEMIC STATUS CLASSIFICATIONS:
546
+ - AFFIRMATIVE: High avg grounding >0.8 (well-established knowledge)
547
+ - CONTESTED: Mixed grounding 0.2-0.8 (debated/mixed validation)
548
+ - CONTRADICTORY: Low grounding <-0.5 (contradicted knowledge)
549
+ - HISTORICAL: Temporal vocabulary (detected by name)
550
+ - INSUFFICIENT_DATA: <3 successful measurements
551
+ - UNCLASSIFIED: Doesn't fit known patterns
552
+
553
+ Use for filtering relationships by epistemic reliability, identifying contested knowledge areas, and curating high-confidence vs exploratory subgraphs.`,
554
+ inputSchema: {
555
+ type: 'object',
556
+ properties: {
557
+ action: {
558
+ type: 'string',
559
+ enum: ['list', 'show', 'measure'],
560
+ description: 'Operation: "list" (all types), "show" (specific type), "measure" (run measurement)',
561
+ },
562
+ // For list action
563
+ status_filter: {
564
+ type: 'string',
565
+ description: 'Filter by status for list action: AFFIRMATIVE, CONTESTED, CONTRADICTORY, HISTORICAL, INSUFFICIENT_DATA, UNCLASSIFIED',
566
+ },
567
+ // For show action
568
+ relationship_type: {
569
+ type: 'string',
570
+ description: 'Relationship type to show (required for show action, e.g., "IMPLIES", "SUPPORTS")',
571
+ },
572
+ // For measure action
573
+ sample_size: {
574
+ type: 'number',
575
+ description: 'Edges to sample per type for measure action (default: 100)',
576
+ default: 100,
577
+ },
578
+ store: {
579
+ type: 'boolean',
580
+ description: 'Store results to database for measure action (default: true)',
581
+ default: true,
582
+ },
583
+ verbose: {
584
+ type: 'boolean',
585
+ description: 'Include detailed statistics for measure action (default: false)',
586
+ default: false,
587
+ },
588
+ },
589
+ required: ['action'],
590
+ },
591
+ },
592
+ {
593
+ name: 'analyze_polarity_axis',
594
+ description: `Analyze bidirectional semantic dimension (polarity axis) between two concept poles (ADR-070).
595
+
596
+ Projects concepts onto an axis formed by opposing semantic poles (e.g., Modern ↔ Traditional, Centralized ↔ Distributed). Returns:
597
+ - Axis quality and magnitude (semantic distinctness)
598
+ - Concept positions along the axis (-1 to +1)
599
+ - Direction distribution (positive/neutral/negative)
600
+ - Grounding correlation patterns
601
+ - Statistical analysis of projections
602
+
603
+ PERFORMANCE: Direct query pattern, ~2-3 seconds execution time.
604
+
605
+ Use Cases:
606
+ - Explore conceptual spectrums and gradients
607
+ - Identify position-grounding correlation patterns
608
+ - Discover concepts balanced between opposing ideas
609
+ - Map semantic dimensions in the knowledge graph`,
610
+ inputSchema: {
611
+ type: 'object',
612
+ properties: {
613
+ positive_pole_id: {
614
+ type: 'string',
615
+ description: 'Concept ID for positive pole (e.g., ID for "Modern")',
616
+ },
617
+ negative_pole_id: {
618
+ type: 'string',
619
+ description: 'Concept ID for negative pole (e.g., ID for "Traditional")',
620
+ },
621
+ candidate_ids: {
622
+ type: 'array',
623
+ items: { type: 'string' },
624
+ description: 'Specific concept IDs to project onto axis (optional)',
625
+ },
626
+ auto_discover: {
627
+ type: 'boolean',
628
+ description: 'Auto-discover related concepts if candidate_ids not provided (default: true)',
629
+ default: true,
630
+ },
631
+ max_candidates: {
632
+ type: 'number',
633
+ description: 'Maximum candidates for auto-discovery (default: 20, max: 100)',
634
+ default: 20,
635
+ },
636
+ max_hops: {
637
+ type: 'number',
638
+ description: 'Maximum graph hops for auto-discovery (1-3, default: 1)',
639
+ default: 1,
640
+ },
641
+ },
642
+ required: ['positive_pole_id', 'negative_pole_id'],
643
+ },
644
+ },
645
+ {
646
+ name: 'artifact',
647
+ description: `Manage saved artifacts (ADR-083). Artifacts persist computed results like search results, projections, and polarity analyses for later recall.
648
+
649
+ Three actions available:
650
+ - "list": List artifacts with optional filtering by type, representation, or ontology
651
+ - "show": Get artifact metadata by ID (without payload)
652
+ - "payload": Get artifact with full payload (for reusing stored analysis)
653
+
654
+ Use artifacts to:
655
+ - Recall previously computed analyses without re-running expensive queries
656
+ - Share analysis results across sessions
657
+ - Track analysis history with parameters and timestamps
658
+ - Check freshness (is_fresh indicates if graph has changed since artifact creation)`,
659
+ inputSchema: {
660
+ type: 'object',
661
+ properties: {
662
+ action: {
663
+ type: 'string',
664
+ enum: ['list', 'show', 'payload'],
665
+ description: 'Operation: "list" (list artifacts), "show" (metadata only), "payload" (full result)',
666
+ },
667
+ // For show and payload
668
+ artifact_id: {
669
+ type: 'number',
670
+ description: 'Artifact ID (required for show, payload)',
671
+ },
672
+ // For list filtering
673
+ artifact_type: {
674
+ type: 'string',
675
+ description: 'Filter by type: search_result, projection, polarity_analysis, query_result, etc.',
676
+ },
677
+ representation: {
678
+ type: 'string',
679
+ description: 'Filter by source: cli, mcp_server, polarity_explorer, embedding_landscape, etc.',
680
+ },
681
+ ontology: {
682
+ type: 'string',
683
+ description: 'Filter by associated ontology name',
684
+ },
685
+ limit: {
686
+ type: 'number',
687
+ description: 'Max artifacts to return for list (default: 20)',
688
+ default: 20,
689
+ },
690
+ offset: {
691
+ type: 'number',
692
+ description: 'Number to skip for pagination (default: 0)',
693
+ default: 0,
694
+ },
695
+ },
696
+ required: ['action'],
697
+ },
698
+ },
699
+ {
700
+ name: 'document',
701
+ description: `Work with documents: list all, show content, or get concepts (ADR-084).
702
+
703
+ Three actions available:
704
+ - "list": List all documents with optional ontology filter
705
+ - "show": Retrieve document content from Garage storage
706
+ - "concepts": Get all concepts extracted from a document
707
+
708
+ Documents are aggregated from source chunks and stored in Garage (S3-compatible storage).
709
+ Use search tool with type="documents" to find documents semantically.`,
710
+ inputSchema: {
711
+ type: 'object',
712
+ properties: {
713
+ action: {
714
+ type: 'string',
715
+ enum: ['list', 'show', 'concepts'],
716
+ description: 'Operation: "list" (all documents), "show" (content), "concepts" (extracted concepts)',
717
+ },
718
+ // For show and concepts
719
+ document_id: {
720
+ type: 'string',
721
+ description: 'Document ID (required for show, concepts). Format: sha256:...',
722
+ },
723
+ // For concepts action
724
+ include_details: {
725
+ type: 'boolean',
726
+ description: 'Include full concept details (evidence, relationships, grounding) in one call. Default: false for lightweight list.',
727
+ default: false,
728
+ },
729
+ // For list
730
+ ontology: {
731
+ type: 'string',
732
+ description: 'Filter by ontology name (for list)',
733
+ },
734
+ limit: {
735
+ type: 'number',
736
+ description: 'Max documents to return for list (default: 50)',
737
+ default: 50,
738
+ },
739
+ offset: {
740
+ type: 'number',
741
+ description: 'Number to skip for pagination (default: 0)',
742
+ default: 0,
743
+ },
744
+ },
745
+ required: ['action'],
746
+ },
747
+ },
748
+ ],
749
+ };
750
+ });
751
+ // List available resources
752
+ server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
753
+ return {
754
+ resources: [
755
+ {
756
+ uri: 'database/stats',
757
+ name: 'Database Statistics',
758
+ description: 'Concept counts, relationship counts, ontology statistics',
759
+ mimeType: 'application/json'
760
+ },
761
+ {
762
+ uri: 'database/info',
763
+ name: 'Database Information',
764
+ description: 'PostgreSQL version, Apache AGE extension details',
765
+ mimeType: 'application/json'
766
+ },
767
+ {
768
+ uri: 'database/health',
769
+ name: 'Database Health',
770
+ description: 'Database connection status, graph availability',
771
+ mimeType: 'application/json'
772
+ },
773
+ {
774
+ uri: 'system/status',
775
+ name: 'System Status',
776
+ description: 'Job scheduler status, resource usage',
777
+ mimeType: 'application/json'
778
+ },
779
+ {
780
+ uri: 'api/health',
781
+ name: 'API Health',
782
+ description: 'API server health and timestamp',
783
+ mimeType: 'application/json'
784
+ },
785
+ {
786
+ uri: 'mcp/allowed-paths',
787
+ name: 'MCP File Access Allowlist',
788
+ description: 'Path allowlist configuration for secure file/directory ingestion (ADR-062)',
789
+ mimeType: 'application/json'
790
+ }
791
+ ]
792
+ };
793
+ });
794
+ // Read resource handler
795
+ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => {
796
+ const { uri } = request.params;
797
+ try {
798
+ switch (uri) {
799
+ case 'database/stats': {
800
+ const result = await client.getDatabaseStats();
801
+ return {
802
+ contents: [{
803
+ uri,
804
+ mimeType: 'text/plain',
805
+ text: (0, formatters_js_1.formatDatabaseStats)(result)
806
+ }]
807
+ };
808
+ }
809
+ case 'database/info': {
810
+ const result = await client.getDatabaseInfo();
811
+ return {
812
+ contents: [{
813
+ uri,
814
+ mimeType: 'text/plain',
815
+ text: (0, formatters_js_1.formatDatabaseInfo)(result)
816
+ }]
817
+ };
818
+ }
819
+ case 'database/health': {
820
+ const result = await client.getDatabaseHealth();
821
+ return {
822
+ contents: [{
823
+ uri,
824
+ mimeType: 'text/plain',
825
+ text: (0, formatters_js_1.formatDatabaseHealth)(result)
826
+ }]
827
+ };
828
+ }
829
+ case 'system/status': {
830
+ const result = await client.getSystemStatus();
831
+ return {
832
+ contents: [{
833
+ uri,
834
+ mimeType: 'text/plain',
835
+ text: (0, formatters_js_1.formatSystemStatus)(result)
836
+ }]
837
+ };
838
+ }
839
+ case 'api/health': {
840
+ const result = await client.health();
841
+ return {
842
+ contents: [{
843
+ uri,
844
+ mimeType: 'text/plain',
845
+ text: (0, formatters_js_1.formatApiHealth)(result)
846
+ }]
847
+ };
848
+ }
849
+ case 'mcp/allowed-paths': {
850
+ const manager = new mcp_allowlist_js_1.McpAllowlistManager();
851
+ const config = manager.getConfig();
852
+ if (!config) {
853
+ return {
854
+ contents: [{
855
+ uri,
856
+ mimeType: 'text/plain',
857
+ text: (0, formatters_js_1.formatMcpAllowedPaths)({
858
+ configured: false,
859
+ message: 'Allowlist not initialized. Run: kg mcp-config init-allowlist',
860
+ hint: 'Initialize with default safe patterns for .md, .txt, .pdf, .png, .jpg files'
861
+ })
862
+ }]
863
+ };
864
+ }
865
+ return {
866
+ contents: [{
867
+ uri,
868
+ mimeType: 'text/plain',
869
+ text: (0, formatters_js_1.formatMcpAllowedPaths)({
870
+ configured: true,
871
+ version: config.version,
872
+ allowed_directories: config.allowed_directories,
873
+ allowed_patterns: config.allowed_patterns,
874
+ blocked_patterns: config.blocked_patterns,
875
+ max_file_size_mb: config.max_file_size_mb,
876
+ max_files_per_directory: config.max_files_per_directory,
877
+ config_path: manager.getAllowlistPath()
878
+ })
879
+ }]
880
+ };
881
+ }
882
+ default:
883
+ throw new Error(`Unknown resource: ${uri}`);
884
+ }
885
+ }
886
+ catch (error) {
887
+ throw new Error(`Failed to read resource ${uri}: ${error.message}`);
888
+ }
889
+ });
890
+ /**
891
+ * Helper: Segment long paths for better readability
892
+ *
893
+ * Paths longer than 5 hops are chunked into segments of 5 hops each.
894
+ * This makes them much easier to read and understand.
895
+ */
896
+ function segmentPath(path) {
897
+ if (!path.hops || path.hops <= 5) {
898
+ return {
899
+ totalHops: path.hops,
900
+ segmented: false,
901
+ nodes: path.nodes,
902
+ relationships: path.relationships,
903
+ };
904
+ }
905
+ // Chunk into segments of 5 hops
906
+ const segments = [];
907
+ const chunkSize = 5;
908
+ const nodes = path.nodes || [];
909
+ const relationships = path.relationships || [];
910
+ for (let i = 0; i < nodes.length; i += chunkSize) {
911
+ segments.push({
912
+ nodes: nodes.slice(i, Math.min(i + chunkSize + 1, nodes.length)),
913
+ relationships: relationships.slice(i, Math.min(i + chunkSize, relationships.length)),
914
+ segment: Math.floor(i / chunkSize) + 1,
915
+ totalSegments: Math.ceil(nodes.length / chunkSize),
916
+ });
917
+ }
918
+ return {
919
+ totalHops: path.hops,
920
+ segmented: true,
921
+ segments,
922
+ };
923
+ }
924
+ // Call tool handler with consolidated routing
925
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
926
+ const { name, arguments: args } = request.params;
927
+ const toolArgs = args || {};
928
+ try {
929
+ switch (name) {
930
+ case 'search': {
931
+ const query = toolArgs.query;
932
+ const searchType = toolArgs.type || 'concepts';
933
+ const limit = toolArgs.limit || 10;
934
+ const min_similarity = toolArgs.min_similarity || DEFAULT_SEARCH_SIMILARITY;
935
+ const offset = toolArgs.offset || 0;
936
+ const ontology = toolArgs.ontology;
937
+ if (searchType === 'sources') {
938
+ // ADR-068 Phase 5: Source text search
939
+ const result = await client.searchSources({
940
+ query,
941
+ limit,
942
+ min_similarity,
943
+ ontology,
944
+ include_concepts: true,
945
+ include_full_text: true,
946
+ });
947
+ const formattedText = (0, formatters_js_1.formatSourceSearchResults)(result);
948
+ return {
949
+ content: [{ type: 'text', text: formattedText }],
950
+ };
951
+ }
952
+ else if (searchType === 'documents') {
953
+ // ADR-084: Document search
954
+ const result = await client.searchDocuments({
955
+ query,
956
+ limit,
957
+ min_similarity,
958
+ ontology,
959
+ });
960
+ // Add query to result for formatting
961
+ const resultWithQuery = { ...result, query };
962
+ const formattedText = (0, formatters_js_1.formatDocumentSearchResults)(resultWithQuery);
963
+ return {
964
+ content: [{ type: 'text', text: formattedText }],
965
+ };
966
+ }
967
+ else {
968
+ // Default: Concept search
969
+ const result = await client.searchConcepts({
970
+ query,
971
+ limit,
972
+ min_similarity,
973
+ offset,
974
+ include_grounding: true,
975
+ include_evidence: true,
976
+ include_diversity: true,
977
+ diversity_max_hops: 2,
978
+ });
979
+ const formattedText = (0, formatters_js_1.formatSearchResults)(result);
980
+ return {
981
+ content: [{ type: 'text', text: formattedText }],
982
+ };
983
+ }
984
+ }
985
+ case 'concept': {
986
+ const action = toolArgs.action;
987
+ switch (action) {
988
+ case 'details': {
989
+ const includeGrounding = toolArgs.include_grounding !== false;
990
+ const includeDiversity = toolArgs.include_diversity === true;
991
+ const diversityMaxHops = toolArgs.diversity_max_hops || 2;
992
+ const truncateEvidence = toolArgs.truncate_evidence !== false; // Default true
993
+ const result = await client.getConceptDetails(toolArgs.concept_id, includeGrounding, includeDiversity, diversityMaxHops);
994
+ const formattedText = (0, formatters_js_1.formatConceptDetails)(result, truncateEvidence);
995
+ return {
996
+ content: [{ type: 'text', text: formattedText }],
997
+ };
998
+ }
999
+ case 'related': {
1000
+ const result = await client.findRelatedConcepts({
1001
+ concept_id: toolArgs.concept_id,
1002
+ max_depth: toolArgs.max_depth || DEFAULT_MAX_DEPTH,
1003
+ relationship_types: toolArgs.relationship_types,
1004
+ // ADR-065: Epistemic status filtering
1005
+ include_epistemic_status: toolArgs.include_epistemic_status,
1006
+ exclude_epistemic_status: toolArgs.exclude_epistemic_status,
1007
+ });
1008
+ const formattedText = (0, formatters_js_1.formatRelatedConcepts)(result);
1009
+ return {
1010
+ content: [{ type: 'text', text: formattedText }],
1011
+ };
1012
+ }
1013
+ case 'connect': {
1014
+ const mode = toolArgs.connection_mode || 'semantic';
1015
+ if (mode === 'exact') {
1016
+ const result = await client.findConnection({
1017
+ from_id: toolArgs.from_id,
1018
+ to_id: toolArgs.to_id,
1019
+ max_hops: toolArgs.max_hops || DEFAULT_MAX_HOPS,
1020
+ // ADR-065: Epistemic status filtering
1021
+ include_epistemic_status: toolArgs.include_epistemic_status,
1022
+ exclude_epistemic_status: toolArgs.exclude_epistemic_status,
1023
+ });
1024
+ // Segment long paths for readability
1025
+ if (result.paths && result.paths.length > 0) {
1026
+ result.paths = result.paths.map(segmentPath);
1027
+ }
1028
+ return {
1029
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1030
+ };
1031
+ }
1032
+ else {
1033
+ const result = await client.findConnectionBySearch({
1034
+ from_query: toolArgs.from_query,
1035
+ to_query: toolArgs.to_query,
1036
+ max_hops: toolArgs.max_hops || DEFAULT_MAX_HOPS,
1037
+ threshold: toolArgs.threshold || DEFAULT_SEMANTIC_THRESHOLD,
1038
+ include_grounding: true,
1039
+ include_evidence: true,
1040
+ // ADR-065: Epistemic status filtering
1041
+ include_epistemic_status: toolArgs.include_epistemic_status,
1042
+ exclude_epistemic_status: toolArgs.exclude_epistemic_status,
1043
+ });
1044
+ const formattedText = (0, formatters_js_1.formatConnectionPaths)(result);
1045
+ return {
1046
+ content: [{ type: 'text', text: formattedText }],
1047
+ };
1048
+ }
1049
+ }
1050
+ default:
1051
+ throw new Error(`Unknown concept action: ${action}`);
1052
+ }
1053
+ }
1054
+ case 'ontology': {
1055
+ const action = toolArgs.action;
1056
+ switch (action) {
1057
+ case 'list': {
1058
+ const result = await client.listOntologies();
1059
+ return {
1060
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1061
+ };
1062
+ }
1063
+ case 'info': {
1064
+ const result = await client.getOntologyInfo(toolArgs.ontology_name);
1065
+ return {
1066
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1067
+ };
1068
+ }
1069
+ case 'files': {
1070
+ const result = await client.getOntologyFiles(toolArgs.ontology_name);
1071
+ return {
1072
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1073
+ };
1074
+ }
1075
+ case 'delete': {
1076
+ const result = await client.deleteOntology(toolArgs.ontology_name, toolArgs.force || false);
1077
+ return {
1078
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1079
+ };
1080
+ }
1081
+ default:
1082
+ throw new Error(`Unknown ontology action: ${action}`);
1083
+ }
1084
+ }
1085
+ case 'job': {
1086
+ const action = toolArgs.action;
1087
+ switch (action) {
1088
+ case 'status': {
1089
+ const result = await client.getJobStatus(toolArgs.job_id);
1090
+ const formattedText = (0, formatters_js_1.formatJobStatus)(result);
1091
+ return {
1092
+ content: [{ type: 'text', text: formattedText }],
1093
+ };
1094
+ }
1095
+ case 'list': {
1096
+ const result = await client.listJobs(toolArgs.status, undefined, toolArgs.limit || 50);
1097
+ const formattedText = (0, formatters_js_1.formatJobList)(result);
1098
+ return {
1099
+ content: [{ type: 'text', text: formattedText }],
1100
+ };
1101
+ }
1102
+ case 'approve': {
1103
+ const result = await client.approveJob(toolArgs.job_id);
1104
+ return {
1105
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1106
+ };
1107
+ }
1108
+ case 'cancel': {
1109
+ const result = await client.cancelJob(toolArgs.job_id);
1110
+ return {
1111
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1112
+ };
1113
+ }
1114
+ default:
1115
+ throw new Error(`Unknown job action: ${action}`);
1116
+ }
1117
+ }
1118
+ case 'ingest': {
1119
+ const action = toolArgs.action;
1120
+ switch (action) {
1121
+ case 'text': {
1122
+ const result = await client.ingestText(toolArgs.text, {
1123
+ ontology: toolArgs.ontology,
1124
+ filename: toolArgs.filename,
1125
+ auto_approve: toolArgs.auto_approve !== undefined ? toolArgs.auto_approve : true,
1126
+ force: toolArgs.force || false,
1127
+ processing_mode: toolArgs.processing_mode || 'serial',
1128
+ options: {
1129
+ target_words: toolArgs.target_words || 1000,
1130
+ overlap_words: toolArgs.overlap_words || 200,
1131
+ },
1132
+ source_type: 'mcp',
1133
+ });
1134
+ return {
1135
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1136
+ };
1137
+ }
1138
+ case 'inspect-file': {
1139
+ const filePath = toolArgs.path;
1140
+ if (!filePath) {
1141
+ throw new Error('path is required');
1142
+ }
1143
+ // Validate against allowlist
1144
+ const manager = new mcp_allowlist_js_1.McpAllowlistManager();
1145
+ const validation = manager.validatePath(filePath);
1146
+ // Expand tilde and resolve path
1147
+ const expandedPath = filePath.startsWith('~')
1148
+ ? path.join(process.env.HOME || '', filePath.slice(1))
1149
+ : filePath;
1150
+ const absolutePath = path.resolve(expandedPath);
1151
+ const result = {
1152
+ path: absolutePath,
1153
+ validation: {
1154
+ allowed: validation.allowed,
1155
+ reason: validation.reason,
1156
+ hint: validation.hint,
1157
+ },
1158
+ };
1159
+ // Get file metadata if exists
1160
+ if (fs.existsSync(absolutePath)) {
1161
+ try {
1162
+ const stats = fs.statSync(absolutePath);
1163
+ const sizeMB = stats.size / (1024 * 1024);
1164
+ result.exists = true;
1165
+ result.metadata = {
1166
+ size_bytes: stats.size,
1167
+ size_mb: parseFloat(sizeMB.toFixed(2)),
1168
+ type: stats.isFile() ? 'file' : stats.isDirectory() ? 'directory' : 'other',
1169
+ modified: stats.mtime.toISOString(),
1170
+ permissions: {
1171
+ readable: fs.constants.R_OK && true,
1172
+ writable: fs.constants.W_OK && true,
1173
+ },
1174
+ };
1175
+ // Detect mime type from extension
1176
+ const ext = path.extname(absolutePath).toLowerCase();
1177
+ const mimeTypes = {
1178
+ '.md': 'text/markdown',
1179
+ '.txt': 'text/plain',
1180
+ '.pdf': 'application/pdf',
1181
+ '.png': 'image/png',
1182
+ '.jpg': 'image/jpeg',
1183
+ '.jpeg': 'image/jpeg',
1184
+ };
1185
+ result.metadata.mime_type = mimeTypes[ext] || 'application/octet-stream';
1186
+ result.metadata.is_image = ext in { '.png': 1, '.jpg': 1, '.jpeg': 1 };
1187
+ }
1188
+ catch (error) {
1189
+ result.exists = true;
1190
+ result.error = `Failed to read metadata: ${error.message}`;
1191
+ }
1192
+ }
1193
+ else {
1194
+ result.exists = false;
1195
+ }
1196
+ return {
1197
+ content: [
1198
+ {
1199
+ type: 'text',
1200
+ text: (0, formatters_js_1.formatInspectFileResult)(result),
1201
+ },
1202
+ ],
1203
+ };
1204
+ }
1205
+ case 'file': {
1206
+ const pathArg = toolArgs.path;
1207
+ const ontology = toolArgs.ontology;
1208
+ const auto_approve = toolArgs.auto_approve !== false;
1209
+ const force = toolArgs.force === true;
1210
+ if (!pathArg || !ontology) {
1211
+ throw new Error('path and ontology are required');
1212
+ }
1213
+ // Support both single path and array of paths
1214
+ const paths = Array.isArray(pathArg) ? pathArg : [pathArg];
1215
+ if (paths.length === 0) {
1216
+ throw new Error('At least one file path is required');
1217
+ }
1218
+ const manager = new mcp_allowlist_js_1.McpAllowlistManager();
1219
+ const results = [];
1220
+ const errors = [];
1221
+ // Process each file
1222
+ for (const filePath of paths) {
1223
+ try {
1224
+ // Validate against allowlist
1225
+ const validation = manager.validatePath(filePath);
1226
+ if (!validation.allowed) {
1227
+ errors.push({
1228
+ file: filePath,
1229
+ error: `File not allowed: ${validation.reason}. ${validation.hint || ''}`,
1230
+ });
1231
+ continue;
1232
+ }
1233
+ // Expand and resolve path
1234
+ const expandedPath = filePath.startsWith('~')
1235
+ ? path.join(process.env.HOME || '', filePath.slice(1))
1236
+ : filePath;
1237
+ const absolutePath = path.resolve(expandedPath);
1238
+ if (!fs.existsSync(absolutePath)) {
1239
+ errors.push({
1240
+ file: absolutePath,
1241
+ error: 'File not found',
1242
+ });
1243
+ continue;
1244
+ }
1245
+ const stats = fs.statSync(absolutePath);
1246
+ if (!stats.isFile()) {
1247
+ errors.push({
1248
+ file: absolutePath,
1249
+ error: 'Path is not a file',
1250
+ });
1251
+ continue;
1252
+ }
1253
+ // Detect if image
1254
+ const ext = path.extname(absolutePath).toLowerCase();
1255
+ const isImage = ['.png', '.jpg', '.jpeg'].includes(ext);
1256
+ const filename = path.basename(absolutePath);
1257
+ // Use unified ingestFile endpoint (same as CLI)
1258
+ // Backend automatically detects file type and routes appropriately
1259
+ const jobResponse = await client.ingestFile(absolutePath, {
1260
+ ontology,
1261
+ filename,
1262
+ auto_approve,
1263
+ force,
1264
+ });
1265
+ results.push({
1266
+ status: 'job_id' in jobResponse ? 'submitted' : 'duplicate',
1267
+ job_id: 'job_id' in jobResponse ? jobResponse.job_id : undefined,
1268
+ duplicate_job_id: 'existing_job_id' in jobResponse ? jobResponse.existing_job_id : undefined,
1269
+ file: absolutePath,
1270
+ type: isImage ? 'image' : 'text',
1271
+ size_bytes: stats.size,
1272
+ ontology,
1273
+ });
1274
+ }
1275
+ catch (error) {
1276
+ errors.push({
1277
+ file: filePath,
1278
+ error: error.message,
1279
+ });
1280
+ }
1281
+ }
1282
+ // Return batch result if multiple files, single result otherwise
1283
+ const result = paths.length > 1 ? {
1284
+ batch: true,
1285
+ ontology,
1286
+ total_files: paths.length,
1287
+ successful: results.length,
1288
+ failed: errors.length,
1289
+ results,
1290
+ errors: errors.length > 0 ? errors : undefined,
1291
+ } : results[0] || errors[0];
1292
+ return {
1293
+ content: [
1294
+ {
1295
+ type: 'text',
1296
+ text: (0, formatters_js_1.formatIngestFileResult)(result),
1297
+ },
1298
+ ],
1299
+ };
1300
+ }
1301
+ case 'directory': {
1302
+ const dirPath = toolArgs.path;
1303
+ const ontology = toolArgs.ontology;
1304
+ const recursive = toolArgs.recursive === true;
1305
+ const auto_approve = toolArgs.auto_approve !== false;
1306
+ const force = toolArgs.force === true;
1307
+ const limit = toolArgs.limit || 10;
1308
+ const offset = toolArgs.offset || 0;
1309
+ if (!dirPath) {
1310
+ throw new Error('path is required');
1311
+ }
1312
+ // Expand and resolve path
1313
+ const expandedPath = dirPath.startsWith('~')
1314
+ ? path.join(process.env.HOME || '', dirPath.slice(1))
1315
+ : dirPath;
1316
+ const absolutePath = path.resolve(expandedPath);
1317
+ if (!fs.existsSync(absolutePath)) {
1318
+ throw new Error(`Directory not found: ${absolutePath}`);
1319
+ }
1320
+ const stats = fs.statSync(absolutePath);
1321
+ if (!stats.isDirectory()) {
1322
+ throw new Error(`Path is not a directory: ${absolutePath}. Use action 'file' for files.`);
1323
+ }
1324
+ // Auto-name ontology from directory if not provided
1325
+ const finalOntology = ontology || path.basename(absolutePath);
1326
+ // Validate directory against allowlist
1327
+ const manager = new mcp_allowlist_js_1.McpAllowlistManager();
1328
+ const dirValidation = manager.validateDirectory(absolutePath);
1329
+ if (!dirValidation.allowed) {
1330
+ throw new Error(`Directory not allowed: ${dirValidation.reason}. ${dirValidation.hint || ''}`);
1331
+ }
1332
+ // Collect files
1333
+ const files = [];
1334
+ const skipped = [];
1335
+ function scanDirectory(dir) {
1336
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1337
+ for (const entry of entries) {
1338
+ const fullPath = path.join(dir, entry.name);
1339
+ if (entry.isDirectory() && recursive) {
1340
+ scanDirectory(fullPath);
1341
+ }
1342
+ else if (entry.isFile()) {
1343
+ // Validate each file
1344
+ const fileValidation = manager.validatePath(fullPath);
1345
+ if (fileValidation.allowed) {
1346
+ files.push(fullPath);
1347
+ }
1348
+ else {
1349
+ skipped.push(fullPath);
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ scanDirectory(absolutePath);
1355
+ // Apply pagination
1356
+ const paginatedFiles = files.slice(offset, offset + limit);
1357
+ // TODO: Implement batch ingestion
1358
+ // For now, return summary with pagination
1359
+ const result = {
1360
+ status: 'not_implemented',
1361
+ message: 'Batch directory ingestion not yet implemented',
1362
+ directory: absolutePath,
1363
+ ontology: finalOntology,
1364
+ files_found: files.length,
1365
+ files_skipped: skipped.length,
1366
+ next_phase: 'Phase 3 - Batch ingestion implementation',
1367
+ files: paginatedFiles,
1368
+ pagination: {
1369
+ offset,
1370
+ limit,
1371
+ total: files.length,
1372
+ has_more: offset + limit < files.length,
1373
+ },
1374
+ };
1375
+ return {
1376
+ content: [
1377
+ {
1378
+ type: 'text',
1379
+ text: (0, formatters_js_1.formatIngestDirectoryResult)(result),
1380
+ },
1381
+ ],
1382
+ };
1383
+ }
1384
+ default:
1385
+ throw new Error(`Unknown ingest action: ${action}`);
1386
+ }
1387
+ }
1388
+ case 'source': {
1389
+ const source_id = toolArgs.source_id;
1390
+ if (!source_id) {
1391
+ throw new Error('source_id is required');
1392
+ }
1393
+ try {
1394
+ // First get metadata to determine content type
1395
+ const metadata = await client.getSourceMetadata(source_id);
1396
+ if (metadata.content_type === 'image') {
1397
+ // Image source - return the image
1398
+ const base64Image = await client.getSourceImageBase64(source_id);
1399
+ return {
1400
+ content: [
1401
+ {
1402
+ type: 'image',
1403
+ data: base64Image,
1404
+ mimeType: 'image/jpeg',
1405
+ },
1406
+ {
1407
+ type: 'text',
1408
+ text: `Retrieved image for source: ${source_id}\n\nDocument: ${metadata.document}\nParagraph: ${metadata.paragraph}\n\nThis image was extracted from the knowledge graph. You can:\n1. Compare the image to extracted concepts to verify accuracy\n2. Create a new description if something was missed\n3. Use ingest action to add refined descriptions`,
1409
+ },
1410
+ ],
1411
+ };
1412
+ }
1413
+ else {
1414
+ // Text source - return metadata with full_text
1415
+ const lines = [
1416
+ `📄 Source: ${source_id}`,
1417
+ '',
1418
+ `Document: ${metadata.document}`,
1419
+ `Paragraph: ${metadata.paragraph}`,
1420
+ `Content Type: ${metadata.content_type || 'text'}`,
1421
+ '',
1422
+ ];
1423
+ if (metadata.char_offset_start !== undefined && metadata.char_offset_end !== undefined) {
1424
+ lines.push(`Character Range: ${metadata.char_offset_start}-${metadata.char_offset_end}`);
1425
+ }
1426
+ if (metadata.file_path) {
1427
+ lines.push(`File Path: ${metadata.file_path}`);
1428
+ }
1429
+ lines.push('', '--- Full Text ---', '', metadata.full_text || '(no text content)');
1430
+ return {
1431
+ content: [{ type: 'text', text: lines.join('\n') }],
1432
+ };
1433
+ }
1434
+ }
1435
+ catch (error) {
1436
+ if (error.response?.status === 404) {
1437
+ throw new Error(`Source ${source_id} not found`);
1438
+ }
1439
+ throw error;
1440
+ }
1441
+ }
1442
+ case 'epistemic_status': {
1443
+ const action = toolArgs.action;
1444
+ switch (action) {
1445
+ case 'list': {
1446
+ const statusFilter = toolArgs.status_filter;
1447
+ const result = await client.listEpistemicStatus(statusFilter);
1448
+ const output = (0, formatters_js_1.formatEpistemicStatusList)(result);
1449
+ return {
1450
+ content: [{ type: 'text', text: output }],
1451
+ };
1452
+ }
1453
+ case 'show': {
1454
+ const relationshipType = toolArgs.relationship_type;
1455
+ if (!relationshipType) {
1456
+ throw new Error('relationship_type is required for show action');
1457
+ }
1458
+ const result = await client.getEpistemicStatus(relationshipType);
1459
+ const output = (0, formatters_js_1.formatEpistemicStatusDetails)(result);
1460
+ return {
1461
+ content: [{ type: 'text', text: output }],
1462
+ };
1463
+ }
1464
+ case 'measure': {
1465
+ const sampleSize = toolArgs.sample_size || 100;
1466
+ const store = toolArgs.store !== false;
1467
+ const verbose = toolArgs.verbose || false;
1468
+ const result = await client.measureEpistemicStatus({
1469
+ sample_size: sampleSize,
1470
+ store: store,
1471
+ verbose: verbose,
1472
+ });
1473
+ const output = (0, formatters_js_1.formatEpistemicStatusMeasurement)(result);
1474
+ return {
1475
+ content: [{ type: 'text', text: output }],
1476
+ };
1477
+ }
1478
+ default:
1479
+ throw new Error(`Unknown epistemic_status action: ${action}`);
1480
+ }
1481
+ }
1482
+ case 'analyze_polarity_axis': {
1483
+ const result = await client.analyzePolarityAxis({
1484
+ positive_pole_id: toolArgs.positive_pole_id,
1485
+ negative_pole_id: toolArgs.negative_pole_id,
1486
+ candidate_ids: toolArgs.candidate_ids,
1487
+ auto_discover: toolArgs.auto_discover !== false,
1488
+ max_candidates: toolArgs.max_candidates || 20,
1489
+ max_hops: toolArgs.max_hops || 2,
1490
+ });
1491
+ const formattedText = (0, formatters_js_1.formatPolarityAxisResults)(result);
1492
+ return {
1493
+ content: [{ type: 'text', text: formattedText }],
1494
+ };
1495
+ }
1496
+ case 'artifact': {
1497
+ const action = toolArgs.action;
1498
+ switch (action) {
1499
+ case 'list': {
1500
+ const result = await client.listArtifacts({
1501
+ artifact_type: toolArgs.artifact_type,
1502
+ representation: toolArgs.representation,
1503
+ ontology: toolArgs.ontology,
1504
+ limit: toolArgs.limit || 20,
1505
+ offset: toolArgs.offset || 0,
1506
+ });
1507
+ // Format artifact list for readability
1508
+ const lines = [
1509
+ `📦 Artifacts (${result.total} total, showing ${result.artifacts.length})`,
1510
+ '',
1511
+ ];
1512
+ if (result.artifacts.length === 0) {
1513
+ lines.push('No artifacts found.');
1514
+ }
1515
+ else {
1516
+ for (const artifact of result.artifacts) {
1517
+ const freshness = artifact.is_fresh ? '✓ Fresh' : '⚠ Stale';
1518
+ lines.push(`[${artifact.id}] ${artifact.artifact_type} - ${artifact.name || '(unnamed)'}`);
1519
+ lines.push(` Representation: ${artifact.representation}`);
1520
+ lines.push(` ${freshness} (epoch: ${artifact.graph_epoch})`);
1521
+ if (artifact.ontology) {
1522
+ lines.push(` Ontology: ${artifact.ontology}`);
1523
+ }
1524
+ lines.push(` Created: ${artifact.created_at}`);
1525
+ lines.push('');
1526
+ }
1527
+ }
1528
+ if (result.total > result.offset + result.artifacts.length) {
1529
+ lines.push(`Use offset=${result.offset + result.limit} to see more.`);
1530
+ }
1531
+ return {
1532
+ content: [{ type: 'text', text: lines.join('\n') }],
1533
+ };
1534
+ }
1535
+ case 'show': {
1536
+ const artifactId = toolArgs.artifact_id;
1537
+ if (!artifactId) {
1538
+ throw new Error('artifact_id is required for show action');
1539
+ }
1540
+ const result = await client.getArtifact(artifactId);
1541
+ // Format artifact metadata
1542
+ const lines = [
1543
+ `📦 Artifact ${result.id}`,
1544
+ '',
1545
+ `Type: ${result.artifact_type}`,
1546
+ `Representation: ${result.representation}`,
1547
+ `Name: ${result.name || '(unnamed)'}`,
1548
+ `Owner ID: ${result.owner_id}`,
1549
+ '',
1550
+ `Freshness: ${result.is_fresh ? '✓ Fresh' : '⚠ Stale (graph changed)'}`,
1551
+ `Graph Epoch: ${result.graph_epoch}`,
1552
+ '',
1553
+ `Created: ${result.created_at}`,
1554
+ result.expires_at ? `Expires: ${result.expires_at}` : '',
1555
+ result.ontology ? `Ontology: ${result.ontology}` : '',
1556
+ '',
1557
+ 'Parameters:',
1558
+ JSON.stringify(result.parameters, null, 2),
1559
+ ].filter(Boolean);
1560
+ return {
1561
+ content: [{ type: 'text', text: lines.join('\n') }],
1562
+ };
1563
+ }
1564
+ case 'payload': {
1565
+ const artifactId = toolArgs.artifact_id;
1566
+ if (!artifactId) {
1567
+ throw new Error('artifact_id is required for payload action');
1568
+ }
1569
+ const result = await client.getArtifactPayload(artifactId);
1570
+ // Return full artifact with payload
1571
+ const lines = [
1572
+ `📦 Artifact ${result.id} (${result.artifact_type})`,
1573
+ `Freshness: ${result.is_fresh ? '✓ Fresh' : '⚠ Stale'}`,
1574
+ '',
1575
+ 'Payload:',
1576
+ JSON.stringify(result.payload, null, 2),
1577
+ ];
1578
+ return {
1579
+ content: [{ type: 'text', text: lines.join('\n') }],
1580
+ };
1581
+ }
1582
+ default:
1583
+ throw new Error(`Unknown artifact action: ${action}`);
1584
+ }
1585
+ }
1586
+ case 'document': {
1587
+ const action = toolArgs.action;
1588
+ switch (action) {
1589
+ case 'list': {
1590
+ const result = await client.listDocuments({
1591
+ ontology: toolArgs.ontology,
1592
+ limit: toolArgs.limit || 50,
1593
+ offset: toolArgs.offset || 0,
1594
+ });
1595
+ const formattedText = (0, formatters_js_1.formatDocumentList)(result);
1596
+ return {
1597
+ content: [{ type: 'text', text: formattedText }],
1598
+ };
1599
+ }
1600
+ case 'show': {
1601
+ const documentId = toolArgs.document_id;
1602
+ if (!documentId) {
1603
+ throw new Error('document_id is required for show action');
1604
+ }
1605
+ const result = await client.getDocumentContent(documentId);
1606
+ const formattedText = (0, formatters_js_1.formatDocumentContent)(result);
1607
+ return {
1608
+ content: [{ type: 'text', text: formattedText }],
1609
+ };
1610
+ }
1611
+ case 'concepts': {
1612
+ const documentId = toolArgs.document_id;
1613
+ if (!documentId) {
1614
+ throw new Error('document_id is required for concepts action');
1615
+ }
1616
+ const includeDetails = toolArgs.include_details === true;
1617
+ const result = await client.getDocumentConcepts(documentId);
1618
+ if (includeDetails) {
1619
+ // Fetch full details for each unique concept
1620
+ const uniqueConceptIds = [...new Set(result.concepts.map((c) => c.concept_id))];
1621
+ const conceptDetails = [];
1622
+ for (const conceptId of uniqueConceptIds) {
1623
+ try {
1624
+ const details = await client.getConceptDetails(conceptId, true);
1625
+ conceptDetails.push(details);
1626
+ }
1627
+ catch (err) {
1628
+ conceptDetails.push({
1629
+ concept_id: conceptId,
1630
+ label: `(failed to load: ${err.message})`,
1631
+ error: true,
1632
+ });
1633
+ }
1634
+ }
1635
+ const formattedText = (0, formatters_js_1.formatDocumentConceptsDetailed)(result, conceptDetails);
1636
+ return {
1637
+ content: [{ type: 'text', text: formattedText }],
1638
+ };
1639
+ }
1640
+ else {
1641
+ const formattedText = (0, formatters_js_1.formatDocumentConcepts)(result);
1642
+ return {
1643
+ content: [{ type: 'text', text: formattedText }],
1644
+ };
1645
+ }
1646
+ }
1647
+ default:
1648
+ throw new Error(`Unknown document action: ${action}`);
1649
+ }
1650
+ }
1651
+ default:
1652
+ throw new Error(`Unknown tool: ${name}`);
1653
+ }
1654
+ }
1655
+ catch (error) {
1656
+ return {
1657
+ content: [
1658
+ {
1659
+ type: 'text',
1660
+ text: JSON.stringify({
1661
+ error: error.message,
1662
+ details: error.response?.data || error.toString(),
1663
+ }, null, 2),
1664
+ },
1665
+ ],
1666
+ isError: true,
1667
+ };
1668
+ }
1669
+ });
1670
+ /**
1671
+ * Prompt Handlers - Provide exploration guidance
1672
+ */
1673
+ server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => {
1674
+ return {
1675
+ prompts: [
1676
+ {
1677
+ name: 'explore-graph',
1678
+ description: 'Learn how to explore the knowledge graph effectively',
1679
+ },
1680
+ ],
1681
+ };
1682
+ });
1683
+ server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => {
1684
+ const { name } = request.params;
1685
+ if (name === 'explore-graph') {
1686
+ return {
1687
+ messages: [
1688
+ {
1689
+ role: 'user',
1690
+ content: {
1691
+ type: 'text',
1692
+ text: `How should I explore this knowledge graph?`,
1693
+ },
1694
+ },
1695
+ {
1696
+ role: 'assistant',
1697
+ content: {
1698
+ type: 'text',
1699
+ text: `# Knowledge Graph Exploration Guide
1700
+
1701
+ This system transforms documents into semantic concept graphs with grounding strength (reliability scores) and evidence (quoted text).
1702
+
1703
+ ## Exploration Workflow:
1704
+
1705
+ 1. **search** - Your entry point. Find concepts by semantic similarity.
1706
+ - Returns RICH DATA:
1707
+ * Grounding strength: Reliability score (-1.0 to 1.0)
1708
+ * Diversity score: % showing conceptual richness
1709
+ * Authenticated diversity: Support/contradiction indicator (✅✓⚠❌)
1710
+ * Evidence samples: Quoted text from sources
1711
+ * Image indicators: Visual evidence available
1712
+ - Use 2-3 word phrases (e.g., "configuration management", "licensing issues")
1713
+
1714
+ 2. **concept (action: connect)** - **PRIORITIZE THIS** - Discover HOW concepts connect.
1715
+ - This is often MORE VALUABLE than isolated concept details
1716
+ - Trace problem→solution chains, cause/effect relationships
1717
+ - See grounding + evidence at each step in the path
1718
+ - Reveals narrative flow through ideas
1719
+ - **PERFORMANCE CRITICAL**: Start with threshold=0.8, max_hops=3
1720
+ - Lower thresholds exponentially increase query time (0.6 = slow, <0.6 = very slow)
1721
+
1722
+ 3. **concept (action: details)** - See the complete picture for any concept.
1723
+ - Returns: ALL quoted evidence + relationships
1724
+ - IMPORTANT: Contradicted concepts (negative grounding) are VALUABLE - they show problems/outdated approaches
1725
+ - Use this after finding interesting concepts via search or connect
1726
+
1727
+ 4. **concept (action: related)** - Explore neighborhoods.
1728
+ - Find what's nearby in the concept graph
1729
+ - Discover clusters and themes
1730
+ - Use depth=1-2 for neighbors, 3-4 for broader exploration
1731
+
1732
+ ## Resources (Fresh Data):
1733
+
1734
+ Use MCP resources for quick status checks without consuming tool budget:
1735
+ - **database/stats** - Concept counts, relationships, ontologies
1736
+ - **database/info** - PostgreSQL version, AGE extension
1737
+ - **database/health** - Connection status
1738
+ - **system/status** - Job scheduler, resource usage
1739
+ - **api/health** - API server status
1740
+
1741
+ ## Understanding the Scores:
1742
+
1743
+ **Grounding Strength (-1.0 to 1.0)** - Reliability/Contradiction
1744
+ - **Positive (>0.7)**: Well-supported, reliable concept
1745
+ - **Moderate (0.3-0.7)**: Mixed evidence, use with caution
1746
+ - **Negative (<0)**: Contradicted or presented as a problem
1747
+ - **Contradicted (-1.0)**: Often the most interesting - shows pain points!
1748
+
1749
+ **Diversity Score (0-100%)** - Conceptual Richness
1750
+ - High diversity: Concept connects to many different related concepts
1751
+ - Shows breadth of connections in the knowledge graph
1752
+ - Higher scores indicate more interconnected, central concepts
1753
+
1754
+ **Authenticated Diversity (✅✓⚠❌)** - Directional Quality
1755
+ - ✅ Diverse support: Strong positive evidence across many connections
1756
+ - ✓ Some support: Moderate positive evidence
1757
+ - ⚠ Weak contradiction: Some conflicting evidence
1758
+ - ❌ Diverse contradiction: Strong negative evidence across connections
1759
+ - Combines grounding with diversity to show reliability + breadth
1760
+
1761
+ ## How to Use the Scores:
1762
+
1763
+ **High Grounding + High Diversity** → Central, well-established concepts
1764
+ - These are reliable anchor points for exploration
1765
+ - Good starting points for connection queries
1766
+
1767
+ **High Grounding + Low Diversity** → Specialized, focused concepts
1768
+ - Domain-specific, niche ideas with strong support
1769
+ - May be isolated or newly added
1770
+
1771
+ **Low/Negative Grounding + High Diversity** → Controversial or evolving concepts
1772
+ - Contested ideas with multiple perspectives
1773
+ - Often the most interesting for understanding debates
1774
+
1775
+ **Authenticated Diversity** → Quick quality check
1776
+ - Use ✅ results confidently
1777
+ - Investigate ⚠ and ❌ results for contradictions and problems
1778
+
1779
+ ## Pro Tips:
1780
+ - **Connection queries first**: After search, immediately explore HOW concepts connect - this reveals causality and narrative
1781
+ - Use diversity scores to find central vs. peripheral concepts
1782
+ - Start broad with search, then drill down with concept details
1783
+ - Contradicted concepts reveal what DIDN'T work - goldmines for learning
1784
+ - Connection paths tell stories - follow the evidence chain
1785
+ - Use resources for quick status checks instead of tools
1786
+ - **Performance**: Keep threshold >= 0.75 for connect queries to avoid slow/hung queries`,
1787
+ },
1788
+ },
1789
+ ],
1790
+ };
1791
+ }
1792
+ throw new Error(`Unknown prompt: ${name}`);
1793
+ });
1794
+ /**
1795
+ * Start the MCP server
1796
+ */
1797
+ async function main() {
1798
+ // Initialize authentication before starting server
1799
+ await initializeAuth();
1800
+ client = createAuthenticatedClient();
1801
+ const transport = new stdio_js_1.StdioServerTransport();
1802
+ await server.connect(transport);
1803
+ // Log to stderr (stdout is used for MCP protocol)
1804
+ console.error('Knowledge Graph MCP Server running on stdio');
1805
+ }
1806
+ main().catch((error) => {
1807
+ console.error('Server error:', error);
1808
+ process.exit(1);
1809
+ });
1810
+ //# sourceMappingURL=mcp-server.js.map