@duckmind/deepquark-darwin-arm64 0.9.83 → 0.9.88

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 (70) hide show
  1. package/.deepquark/skills/bundled/knowledge-graph/SKILL.md +385 -0
  2. package/.deepquark/skills/bundled/knowledge-graph/STANDARDS.md +461 -0
  3. package/.deepquark/skills/bundled/knowledge-graph/lib/cli.ts +588 -0
  4. package/.deepquark/skills/bundled/knowledge-graph/lib/config.ts +630 -0
  5. package/.deepquark/skills/bundled/knowledge-graph/lib/connection-profile.ts +629 -0
  6. package/.deepquark/skills/bundled/knowledge-graph/lib/container.ts +756 -0
  7. package/.deepquark/skills/bundled/knowledge-graph/lib/mcp-client.ts +1310 -0
  8. package/.deepquark/skills/bundled/knowledge-graph/lib/output-formatter.ts +997 -0
  9. package/.deepquark/skills/bundled/knowledge-graph/lib/token-metrics.ts +335 -0
  10. package/.deepquark/skills/bundled/knowledge-graph/lib/transformation-log.ts +137 -0
  11. package/.deepquark/skills/bundled/knowledge-graph/lib/wrapper-config.ts +113 -0
  12. package/.deepquark/skills/bundled/knowledge-graph/server/.env.example +129 -0
  13. package/.deepquark/skills/bundled/knowledge-graph/server/compare-embeddings.ts +175 -0
  14. package/.deepquark/skills/bundled/knowledge-graph/server/config-falkordb.yaml +108 -0
  15. package/.deepquark/skills/bundled/knowledge-graph/server/config-neo4j.yaml +111 -0
  16. package/.deepquark/skills/bundled/knowledge-graph/server/diagnose.ts +483 -0
  17. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb-dev.yml +146 -0
  18. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-falkordb.yml +151 -0
  19. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev-local.yml +161 -0
  20. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j-dev.yml +161 -0
  21. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-neo4j.yml +169 -0
  22. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-production.yml +128 -0
  23. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose-test.yml +10 -0
  24. package/.deepquark/skills/bundled/knowledge-graph/server/docker-compose.yml +84 -0
  25. package/.deepquark/skills/bundled/knowledge-graph/server/entrypoint.sh +40 -0
  26. package/.deepquark/skills/bundled/knowledge-graph/server/install.ts +2054 -0
  27. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-falkordb.yml +78 -0
  28. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose-neo4j.yml +88 -0
  29. package/.deepquark/skills/bundled/knowledge-graph/server/podman-compose.yml +83 -0
  30. package/.deepquark/skills/bundled/knowledge-graph/server/test-all-llms-mcp.ts +387 -0
  31. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-models.ts +201 -0
  32. package/.deepquark/skills/bundled/knowledge-graph/server/test-embedding-providers.ts +641 -0
  33. package/.deepquark/skills/bundled/knowledge-graph/server/test-graphiti-model.ts +217 -0
  34. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-correct.ts +141 -0
  35. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-llms-mcp.ts +386 -0
  36. package/.deepquark/skills/bundled/knowledge-graph/server/test-grok-models.ts +173 -0
  37. package/.deepquark/skills/bundled/knowledge-graph/server/test-llama-extraction.ts +188 -0
  38. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-final.ts +240 -0
  39. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-live.ts +187 -0
  40. package/.deepquark/skills/bundled/knowledge-graph/server/test-mcp-session.ts +127 -0
  41. package/.deepquark/skills/bundled/knowledge-graph/server/test-model-combinations.ts +316 -0
  42. package/.deepquark/skills/bundled/knowledge-graph/server/test-ollama-models.ts +228 -0
  43. package/.deepquark/skills/bundled/knowledge-graph/server/test-openrouter-models.ts +460 -0
  44. package/.deepquark/skills/bundled/knowledge-graph/server/test-real-life-mcp.ts +311 -0
  45. package/.deepquark/skills/bundled/knowledge-graph/server/test-search-debug.ts +199 -0
  46. package/.deepquark/skills/bundled/knowledge-graph/tools/Install.md +104 -0
  47. package/.deepquark/skills/bundled/knowledge-graph/tools/README.md +120 -0
  48. package/.deepquark/skills/bundled/knowledge-graph/tools/knowledge-cli.ts +996 -0
  49. package/.deepquark/skills/bundled/knowledge-graph/tools/server-cli.ts +531 -0
  50. package/.deepquark/skills/bundled/knowledge-graph/workflows/BulkImport.md +514 -0
  51. package/.deepquark/skills/bundled/knowledge-graph/workflows/CaptureEpisode.md +242 -0
  52. package/.deepquark/skills/bundled/knowledge-graph/workflows/ClearGraph.md +392 -0
  53. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetRecent.md +352 -0
  54. package/.deepquark/skills/bundled/knowledge-graph/workflows/GetStatus.md +373 -0
  55. package/.deepquark/skills/bundled/knowledge-graph/workflows/HealthReport.md +212 -0
  56. package/.deepquark/skills/bundled/knowledge-graph/workflows/InvestigateEntity.md +142 -0
  57. package/.deepquark/skills/bundled/knowledge-graph/workflows/OntologyManagement.md +201 -0
  58. package/.deepquark/skills/bundled/knowledge-graph/workflows/RunMaintenance.md +302 -0
  59. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchByDate.md +255 -0
  60. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchFacts.md +382 -0
  61. package/.deepquark/skills/bundled/knowledge-graph/workflows/SearchKnowledge.md +374 -0
  62. package/.deepquark/skills/bundled/knowledge-graph/workflows/StixImport.md +212 -0
  63. package/bin/deepquark +0 -0
  64. package/package.json +1 -1
  65. package/.deepquark/skills/bundled/ge-payroll/SKILL.md +0 -153
  66. package/.deepquark/skills/bundled/ge-payroll/evals/evals.json +0 -23
  67. package/.deepquark/skills/bundled/ge-payroll/references/pain-points-improvements.md +0 -106
  68. package/.deepquark/skills/bundled/ge-payroll/references/process-detail.md +0 -217
  69. package/.deepquark/skills/bundled/ge-payroll/references/raci-stakeholders.md +0 -85
  70. package/.deepquark/skills/bundled/ge-payroll/references/timeline-mandays.md +0 -64
@@ -0,0 +1,1310 @@
1
+ /**
2
+ * MCP Client Library
3
+ *
4
+ * HTTP client for communicating with the Graphiti MCP server.
5
+ * Handles JSON-RPC 2.0 requests for all MCP tools.
6
+ *
7
+ * NOTE: Lucene sanitization is now handled server-side by the Python patch
8
+ * (falkordb_lucene.py). The TypeScript client passes values directly to the server.
9
+ */
10
+
11
+ // Import profile loading function for connection profiles
12
+ import { loadProfileWithOverrides } from './connection-profile.js';
13
+
14
+ /**
15
+ * MCP tool names (Graphiti MCP server)
16
+ *
17
+ * Note: The Graphiti MCP server tools:
18
+ * - add_memory: Add episodes to the knowledge graph
19
+ * - search_nodes: Search for entities/nodes
20
+ * - search_memory_facts: Search for relationships/facts
21
+ * - get_episodes: Retrieve recent episodes
22
+ * - delete_episode, delete_entity_edge: Deletion operations
23
+ * - clear_graph: Clear all data
24
+ * - get_status: Server health check
25
+ *
26
+ * TypeScript methods use Graphiti-native terminology (Episode, Node, Fact)
27
+ * while calling the actual MCP tool names.
28
+ */
29
+ export const MCP_TOOLS = {
30
+ // Knowledge capture (adds an "episode" to memory)
31
+ ADD_EPISODE: 'add_memory',
32
+ // Entity search (searches "nodes")
33
+ SEARCH_NODES: 'search_nodes',
34
+ // Relationship search (searches "facts" in memory)
35
+ SEARCH_FACTS: 'search_memory_facts',
36
+ // Episode retrieval
37
+ GET_EPISODES: 'get_episodes',
38
+ // System operations
39
+ GET_STATUS: 'get_status',
40
+ CLEAR_GRAPH: 'clear_graph',
41
+ DELETE_EPISODE: 'delete_episode',
42
+ DELETE_ENTITY_EDGE: 'delete_entity_edge',
43
+ GET_ENTITY_EDGE: 'get_entity_edge',
44
+ // Feature 009: Memory decay scoring
45
+ GET_KNOWLEDGE_HEALTH: 'get_knowledge_health',
46
+ RUN_DECAY_MAINTENANCE: 'run_decay_maintenance',
47
+ CLASSIFY_MEMORY: 'classify_memory',
48
+ RECOVER_SOFT_DELETED: 'recover_soft_deleted',
49
+ // Feature 018: OSINT/CTI Ontology support
50
+ LIST_ONTOLOGY_TYPES: 'list_ontology_types',
51
+ VALIDATE_ONTOLOGY: 'validate_ontology',
52
+ RELOAD_ONTOLOGY: 'reload_ontology',
53
+ // Feature 018: STIX 2.1 import
54
+ IMPORT_STIX_BUNDLE: 'import_stix_bundle',
55
+ GET_IMPORT_STATUS: 'get_import_status',
56
+ RESUME_IMPORT: 'resume_import',
57
+ // Feature 020: Investigative search
58
+ INVESTIGATE_ENTITY: 'investigate_entity',
59
+ } as const;
60
+
61
+ /**
62
+ * MCP tool parameters
63
+ */
64
+ export interface AddEpisodeParams {
65
+ name: string;
66
+ episode_body: string;
67
+ source?: string;
68
+ reference_timestamp?: string;
69
+ source_description?: string;
70
+ }
71
+
72
+ export interface SearchNodesParams {
73
+ query: string;
74
+ /** Maximum number of nodes to return (maps to max_nodes on server) */
75
+ limit?: number;
76
+ group_ids?: string[];
77
+ /** Filter by entity type names (e.g., ["Preference", "Procedure"]) */
78
+ entity_types?: string[];
79
+ /** Return nodes created after this date (ISO 8601 or relative: "today", "7d", "1 week ago") */
80
+ since?: string;
81
+ /** Return nodes created before this date (ISO 8601 or relative) */
82
+ until?: string;
83
+ /** Apply weighted scoring (60% semantic + 25% recency + 15% importance) */
84
+ include_weighted_scores?: boolean;
85
+ }
86
+
87
+ export interface SearchFactsParams {
88
+ query: string;
89
+ limit?: number;
90
+ group_ids?: string[];
91
+ max_facts?: number;
92
+ /** Filter by entity type (e.g., "Preference", "Procedure", "Learning", "Research", "Decision") */
93
+ entity?: string;
94
+ /** Center the search on a specific entity UUID */
95
+ center_node_uuid?: string;
96
+ /** Return facts created after this date (ISO 8601 or relative: "today", "7d", "1 week ago") */
97
+ since?: string;
98
+ /** Return facts created before this date (ISO 8601 or relative) */
99
+ until?: string;
100
+ }
101
+
102
+ export interface GetEpisodesParams {
103
+ /** Maximum number of episodes to return (maps to max_episodes on server) */
104
+ limit?: number;
105
+ /** Single group ID (will be converted to group_ids array) */
106
+ group_id?: string;
107
+ /** Multiple group IDs */
108
+ group_ids?: string[];
109
+ }
110
+
111
+ export type GetStatusParams = Record<string, never>;
112
+
113
+ export type ClearGraphParams = Record<string, never>;
114
+
115
+ export interface DeleteEpisodeParams {
116
+ uuid: string;
117
+ }
118
+
119
+ export interface DeleteEntityEdgeParams {
120
+ uuid: string;
121
+ }
122
+
123
+ export interface GetEntityEdgeParams {
124
+ uuid: string;
125
+ }
126
+
127
+ // Feature 009: Memory decay scoring parameters
128
+ export interface GetKnowledgeHealthParams {
129
+ group_id?: string;
130
+ }
131
+
132
+ export interface RunDecayMaintenanceParams {
133
+ dry_run?: boolean;
134
+ }
135
+
136
+ export interface ClassifyMemoryParams {
137
+ content: string;
138
+ source_description?: string;
139
+ }
140
+
141
+ export interface RecoverSoftDeletedParams {
142
+ uuid: string;
143
+ }
144
+
145
+ /**
146
+ * Feature 020: Investigative search parameters
147
+ */
148
+ export interface InvestigateEntityParams {
149
+ entity_name: string;
150
+ max_depth?: number;
151
+ relationship_types?: string[];
152
+ group_ids?: string[];
153
+ include_attributes?: boolean;
154
+ }
155
+
156
+ /**
157
+ * JSON-RPC 2.0 request
158
+ */
159
+ export interface JSONRPCRequest {
160
+ jsonrpc: '2.0';
161
+ id: number | string;
162
+ method: string;
163
+ params: {
164
+ name: string;
165
+ arguments: Record<string, unknown>;
166
+ };
167
+ }
168
+
169
+ /**
170
+ * JSON-RPC 2.0 response
171
+ */
172
+ export interface JSONRPCResponse {
173
+ jsonrpc: '2.0';
174
+ id: number | string;
175
+ result?: unknown;
176
+ error?: {
177
+ code: number;
178
+ message: string;
179
+ data?: unknown;
180
+ };
181
+ }
182
+
183
+ /**
184
+ * MCP client response
185
+ */
186
+ export interface MCPClientResponse<T = unknown> {
187
+ success: boolean;
188
+ data?: T;
189
+ error?: string;
190
+ code?: number;
191
+ }
192
+
193
+ /**
194
+ * TLS/SSL configuration for HTTPS connections
195
+ */
196
+ export interface TLSConfig {
197
+ /** Enable certificate verification (default: true) */
198
+ verify?: boolean;
199
+ /** Path to CA certificate file (PEM format) */
200
+ ca?: string;
201
+ /** Path to client certificate file (PEM format) */
202
+ cert?: string;
203
+ /** Path to client private key file (PEM format) */
204
+ key?: string;
205
+ /** Minimum TLS protocol version (default: TLSv1.2) */
206
+ minVersion?: 'TLSv1.2' | 'TLSv1.3';
207
+ }
208
+
209
+ /**
210
+ * MCP Client configuration
211
+ */
212
+ export interface MCPClientConfig {
213
+ /** Base URL for MCP server (deprecated: use protocol+host+port+basePath) */
214
+ baseURL?: string;
215
+ /** Protocol: http or https (default: http) */
216
+ protocol?: 'http' | 'https';
217
+ /** Hostname or IP address (default: localhost) */
218
+ host?: string;
219
+ /** TCP port (default: 8001) */
220
+ port?: number;
221
+ /** URL path prefix (default: /mcp) */
222
+ basePath?: string;
223
+ /** Request timeout in milliseconds (default: 30000) */
224
+ timeout?: number;
225
+ /** Custom headers */
226
+ headers?: Record<string, string>;
227
+ /** TLS configuration (required if protocol=https) */
228
+ tls?: TLSConfig;
229
+ /** Connection profile name (loads from file) */
230
+ profile?: string;
231
+ }
232
+
233
+ /**
234
+ * Default timeout for requests
235
+ */
236
+ const DEFAULT_TIMEOUT = 30000; // 30 seconds
237
+
238
+ /**
239
+ * Simple LRU Cache for search results
240
+ */
241
+ interface CacheEntry<T> {
242
+ data: T;
243
+ timestamp: number;
244
+ }
245
+
246
+ class LRUCache<T> {
247
+ private cache: Map<string, CacheEntry<T>>;
248
+ private maxSize: number;
249
+ private ttlMs: number;
250
+
251
+ constructor(maxSize = 100, ttlMs: number = 5 * 60 * 1000) {
252
+ this.cache = new Map();
253
+ this.maxSize = maxSize;
254
+ this.ttlMs = ttlMs;
255
+ }
256
+
257
+ get(key: string): T | undefined {
258
+ const entry = this.cache.get(key);
259
+ if (!entry) return undefined;
260
+
261
+ // Check TTL
262
+ if (Date.now() - entry.timestamp > this.ttlMs) {
263
+ this.cache.delete(key);
264
+ return undefined;
265
+ }
266
+
267
+ // Move to end (most recently used)
268
+ this.cache.delete(key);
269
+ this.cache.set(key, entry);
270
+ return entry.data;
271
+ }
272
+
273
+ set(key: string, data: T): void {
274
+ // Evict oldest if at capacity
275
+ if (this.cache.size >= this.maxSize) {
276
+ const oldestKey = this.cache.keys().next().value;
277
+ if (oldestKey) this.cache.delete(oldestKey);
278
+ }
279
+
280
+ this.cache.set(key, { data, timestamp: Date.now() });
281
+ }
282
+
283
+ clear(): void {
284
+ this.cache.clear();
285
+ }
286
+
287
+ size(): number {
288
+ return this.cache.size;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Extended MCP Client configuration with caching options
294
+ */
295
+ export interface MCPClientConfigExtended extends MCPClientConfig {
296
+ enableCache?: boolean;
297
+ cacheMaxSize?: number;
298
+ cacheTtlMs?: number;
299
+ }
300
+
301
+ /**
302
+ * Create HTTPS agent with custom TLS options for Bun/Node.js fetch
303
+ *
304
+ * T021 [P] [US2]: Create HTTPS agent factory with custom TLS options
305
+ * T022 [US2]: Implement TLS certificate verification logic
306
+ */
307
+ function createHTTPSOptions(tls?: TLSConfig): RequestInit {
308
+ const options: RequestInit = {};
309
+
310
+ if (!tls) {
311
+ return options;
312
+ }
313
+
314
+ // For Bun, we use a different approach with custom agent
315
+ // Bun's fetch doesn't directly support https.Agent, but we can pass
316
+ // TLS configuration through the request context
317
+
318
+ // T022 [US2]: Implement TLS certificate verification logic
319
+ // Default is to verify certificates (secure by default)
320
+ const verify = tls.verify !== false;
321
+
322
+ // For development/testing: allow self-signed certificates
323
+ if (!verify) {
324
+ // Bun doesn't have a direct way to disable verification in fetch
325
+ // This is a known limitation - users should use valid certificates
326
+ // or configure their system to trust the self-signed certificate
327
+ if (process.env.DEBUG === '1') {
328
+ console.warn('TLS certificate verification is DISABLED. This is not secure for production.');
329
+ }
330
+ }
331
+
332
+ // T024 [US2]: Add MADEINOZ_KNOWLEDGE_TLS_CA environment variable support
333
+ // T023 [US2]: Add MADEINOZ_KNOWLEDGE_TLS_VERIFY environment variable support
334
+ // Note: Bun's fetch API doesn't directly support custom CA certificates
335
+ // Users need to configure system-level certificate trust or use a proxy
336
+ if (tls.ca && process.env.DEBUG === '1') {
337
+ console.warn(`Custom CA certificate specified: ${tls.ca}`);
338
+ console.warn('Bun fetch requires system-level certificate configuration. Use NODE_OPTIONS=--use-openssl-ca or configure certificate trust at OS level.');
339
+ }
340
+
341
+ // Client certificate authentication (mTLS)
342
+ if (tls.cert && tls.key && process.env.DEBUG === '1') {
343
+ console.warn(`Client certificates specified: cert=${tls.cert}, key=${tls.key}`);
344
+ console.warn('Bun fetch does not directly support client certificates. Consider using Node.js for mTLS support.');
345
+ }
346
+
347
+ return options;
348
+ }
349
+
350
+ /**
351
+ * MCP Client class with session management and optional response caching
352
+ */
353
+ export class MCPClient {
354
+ private baseURL: string;
355
+ private timeout: number;
356
+ private headers: Record<string, string>;
357
+ private requestId: number;
358
+ private cache: LRUCache<unknown> | null;
359
+ private sessionId: string | null = null;
360
+ private initializePromise: Promise<void> | null = null;
361
+ private tlsConfig: TLSConfig | undefined;
362
+
363
+ constructor(config: MCPClientConfigExtended = {}) {
364
+ // Construct baseURL from protocol+host+port+basePath if baseURL not provided
365
+ if (config.baseURL) {
366
+ // Backward compatibility: use explicit baseURL
367
+ this.baseURL = config.baseURL;
368
+ } else {
369
+ // Construct from individual components
370
+ const protocol = config.protocol || 'http';
371
+ const host = config.host || 'localhost';
372
+ const port = config.port || 8001;
373
+ const basePath = config.basePath || '/mcp';
374
+ this.baseURL = `${protocol}://${host}:${port}${basePath}`;
375
+ }
376
+
377
+ this.timeout = config.timeout || DEFAULT_TIMEOUT;
378
+ this.headers = {
379
+ 'Content-Type': 'application/json',
380
+ Accept: 'application/json, text/event-stream',
381
+ ...config.headers,
382
+ };
383
+ this.requestId = 1;
384
+
385
+ // Store TLS configuration for HTTPS connections
386
+ this.tlsConfig = config.tls;
387
+
388
+ // TLS configuration is active - file paths are not logged for security
389
+ // To debug TLS issues, temporarily enable: DEBUG=1 bun run ...
390
+ if (this.baseURL.startsWith('https://')) {
391
+ const verify = this.tlsConfig?.verify !== false;
392
+ if (process.env.DEBUG === '1' && !verify) {
393
+ console.warn('[MCPClient] TLS verification disabled - not secure for production');
394
+ }
395
+ }
396
+
397
+ // Initialize cache if enabled (default: enabled for search operations)
398
+ if (config.enableCache !== false) {
399
+ this.cache = new LRUCache<unknown>(
400
+ config.cacheMaxSize || 100,
401
+ config.cacheTtlMs || 5 * 60 * 1000 // 5 minutes default
402
+ );
403
+ } else {
404
+ this.cache = null;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Initialize MCP session and get session ID
410
+ */
411
+ private async initializeSession(): Promise<void> {
412
+ if (this.sessionId) return;
413
+ if (this.initializePromise) return this.initializePromise;
414
+
415
+ this.initializePromise = (async () => {
416
+ const request = {
417
+ jsonrpc: '2.0',
418
+ id: this.requestId++,
419
+ method: 'initialize',
420
+ params: {
421
+ protocolVersion: '2024-11-05',
422
+ capabilities: {},
423
+ clientInfo: { name: 'mcp-wrapper', version: '1.0.0' },
424
+ },
425
+ };
426
+
427
+ // T021 [P] [US2]: Apply TLS configuration for HTTPS requests
428
+ const tlsOptions = createHTTPSOptions(this.tlsConfig);
429
+
430
+ const response = await fetch(this.baseURL, {
431
+ method: 'POST',
432
+ headers: this.headers,
433
+ body: JSON.stringify(request),
434
+ ...tlsOptions,
435
+ });
436
+
437
+ if (!response.ok) {
438
+ throw new Error(`Failed to initialize session: HTTP ${response.status}`);
439
+ }
440
+
441
+ // Get session ID from header
442
+ const sessionId = response.headers.get('Mcp-Session-Id');
443
+ if (!sessionId) {
444
+ throw new Error('Server did not return session ID');
445
+ }
446
+ this.sessionId = sessionId;
447
+
448
+ // Consume the SSE response body
449
+ await response.text();
450
+
451
+ // Send initialized notification (required by FastMCP HTTP transport)
452
+ const notifyRequest = {
453
+ jsonrpc: '2.0',
454
+ method: 'notifications/initialized',
455
+ };
456
+ await fetch(this.baseURL, {
457
+ method: 'POST',
458
+ headers: {
459
+ ...this.headers,
460
+ 'Mcp-Session-Id': this.sessionId,
461
+ },
462
+ body: JSON.stringify(notifyRequest),
463
+ });
464
+ })();
465
+
466
+ await this.initializePromise;
467
+ }
468
+
469
+ /**
470
+ * Parse SSE response to extract JSON-RPC result
471
+ */
472
+ private parseSSEResponse(text: string): unknown {
473
+ // SSE format: "event: message\ndata: {...}\n\n"
474
+ const lines = text.split('\n');
475
+ for (const line of lines) {
476
+ if (line.startsWith('data: ')) {
477
+ const jsonStr = line.substring(6);
478
+ try {
479
+ const parsed = JSON.parse(jsonStr);
480
+ // Extract result from the MCP response format
481
+ if (parsed.result) {
482
+ // Handle structured Pydantic responses from FastMCP (NodeSearchResponse, etc.)
483
+ // These have direct fields (nodes, facts, episodes, status) not wrapped in content array
484
+ if (parsed.result.nodes && Array.isArray(parsed.result.nodes)) {
485
+ // NodeSearchResponse - return as-is with nodes array
486
+ return parsed.result;
487
+ }
488
+ if (parsed.result.facts && Array.isArray(parsed.result.facts)) {
489
+ // FactSearchResponse - return as-is with facts array
490
+ return parsed.result;
491
+ }
492
+ if (parsed.result.episodes && Array.isArray(parsed.result.episodes)) {
493
+ // EpisodeSearchResponse - return as-is with episodes array
494
+ return parsed.result;
495
+ }
496
+ if (parsed.result.status !== undefined) {
497
+ // StatusResponse - return as-is
498
+ return parsed.result;
499
+ }
500
+
501
+ // Handle tool call response format (content array)
502
+ if (parsed.result.content && Array.isArray(parsed.result.content)) {
503
+ // Check for structuredContent first (preferred)
504
+ if (parsed.result.structuredContent) {
505
+ const sc = parsed.result.structuredContent;
506
+ // Unwrap Graphiti's result wrapper if present
507
+ if (sc.result && typeof sc.result === 'object') {
508
+ return sc.result;
509
+ }
510
+ return sc;
511
+ }
512
+ // Fall back to text content
513
+ const textContent = parsed.result.content.find(
514
+ (c: { type: string }) => c.type === 'text'
515
+ );
516
+ if (textContent?.text) {
517
+ try {
518
+ const textParsed = JSON.parse(textContent.text);
519
+ // Unwrap Graphiti's result wrapper if present
520
+ if (textParsed.result && typeof textParsed.result === 'object') {
521
+ return textParsed.result;
522
+ }
523
+ return textParsed;
524
+ } catch {
525
+ return textContent.text;
526
+ }
527
+ }
528
+ }
529
+ return parsed.result;
530
+ }
531
+ if (parsed.error) {
532
+ throw new Error(parsed.error.message || 'Unknown error');
533
+ }
534
+ return parsed;
535
+ } catch (e) {
536
+ if (e instanceof SyntaxError) continue;
537
+ throw e;
538
+ }
539
+ }
540
+ }
541
+ throw new Error('No valid SSE data found in response');
542
+ }
543
+
544
+ /**
545
+ * Generate cache key for a tool call
546
+ */
547
+ private getCacheKey(toolName: string, args: Record<string, unknown>): string {
548
+ return `${toolName}:${JSON.stringify(args)}`;
549
+ }
550
+
551
+ /**
552
+ * Clear the response cache
553
+ */
554
+ clearCache(): void {
555
+ this.cache?.clear();
556
+ }
557
+
558
+ /**
559
+ * Get cache statistics
560
+ */
561
+ getCacheStats(): { enabled: boolean; size: number } {
562
+ return {
563
+ enabled: this.cache !== null,
564
+ size: this.cache?.size() || 0,
565
+ };
566
+ }
567
+
568
+ /**
569
+ * Call an MCP tool
570
+ */
571
+ async callTool<T = unknown>(
572
+ toolName: string,
573
+ arguments_: Record<string, unknown>
574
+ ): Promise<MCPClientResponse<T>> {
575
+ try {
576
+ // Ensure session is initialized
577
+ await this.initializeSession();
578
+
579
+ const request: JSONRPCRequest = {
580
+ jsonrpc: '2.0',
581
+ id: this.requestId++,
582
+ method: 'tools/call',
583
+ params: {
584
+ name: toolName,
585
+ arguments: arguments_,
586
+ },
587
+ };
588
+
589
+ const controller = new AbortController();
590
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
591
+
592
+ // T021 [P] [US2]: Create HTTPS agent factory with custom TLS options
593
+ // Apply TLS configuration for HTTPS requests
594
+ const tlsOptions = createHTTPSOptions(this.tlsConfig);
595
+
596
+ const response = await fetch(this.baseURL, {
597
+ method: 'POST',
598
+ headers: {
599
+ ...this.headers,
600
+ 'Mcp-Session-Id': this.sessionId!,
601
+ },
602
+ body: JSON.stringify(request),
603
+ signal: controller.signal,
604
+ ...tlsOptions,
605
+ });
606
+
607
+ clearTimeout(timeoutId);
608
+
609
+ if (!response.ok) {
610
+ return {
611
+ success: false,
612
+ error: `HTTP ${response.status}: ${response.statusText}`,
613
+ code: response.status,
614
+ };
615
+ }
616
+
617
+ // Parse SSE response
618
+ const text = await response.text();
619
+ const data = this.parseSSEResponse(text);
620
+
621
+ return {
622
+ success: true,
623
+ data: data as T,
624
+ };
625
+ } catch (error: unknown) {
626
+ if (error instanceof Error) {
627
+ if (error.name === 'AbortError') {
628
+ return {
629
+ success: false,
630
+ error: `Request timeout after ${this.timeout}ms`,
631
+ };
632
+ }
633
+
634
+ // T026 [US2]: Add TLS certificate error handling with clear messages
635
+ const errorMsg = error.message.toLowerCase();
636
+ if (
637
+ errorMsg.includes('certificate') ||
638
+ errorMsg.includes('tls') ||
639
+ errorMsg.includes('ssl') ||
640
+ errorMsg.includes('handshake') ||
641
+ errorMsg.includes('certificate verify failed')
642
+ ) {
643
+ const suggestions: string[] = [];
644
+ if (this.tlsConfig?.ca) {
645
+ suggestions.push(`Verify CA certificate path exists: ${this.tlsConfig.ca}`);
646
+ }
647
+ if (this.tlsConfig?.verify === false) {
648
+ suggestions.push('TLS verification is disabled but certificate error still occurred');
649
+ } else {
650
+ suggestions.push('Try setting MADEINOZ_KNOWLEDGE_TLS_VERIFY=false for self-signed certificates (not recommended for production)');
651
+ suggestions.push('Ensure the server certificate is valid and trusted');
652
+ }
653
+ return {
654
+ success: false,
655
+ error: `TLS Certificate Error: ${error.message}${suggestions.length > 0 ? '\nSuggestions:\n - ' + suggestions.join('\n - ') : ''}`,
656
+ };
657
+ }
658
+
659
+ // Host unreachable errors
660
+ if (
661
+ errorMsg.includes('econnrefused') ||
662
+ errorMsg.includes('connection refused') ||
663
+ errorMsg.includes('econnreset')
664
+ ) {
665
+ return {
666
+ success: false,
667
+ error: `Connection Error: Unable to reach server at ${this.baseURL}\n - Verify the server is running\n - Check firewall settings\n - Verify host and port are correct`,
668
+ };
669
+ }
670
+
671
+ return {
672
+ success: false,
673
+ error: error.message,
674
+ };
675
+ }
676
+ return {
677
+ success: false,
678
+ error: 'Unknown error occurred',
679
+ };
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Add an episode to the knowledge graph
685
+ */
686
+ async addEpisode(params: AddEpisodeParams): Promise<MCPClientResponse<{ uuid: string }>> {
687
+ return await this.callTool<{ uuid: string }>(MCP_TOOLS.ADD_EPISODE, params as unknown as Record<string, unknown>);
688
+ }
689
+
690
+ /**
691
+ * Search for nodes (entities) in the knowledge graph
692
+ * Results are cached for repeated queries
693
+ */
694
+ async searchNodes(params: SearchNodesParams): Promise<MCPClientResponse<unknown[]>> {
695
+ // Build server params with correct field names
696
+ // Sanitization is handled server-side by falkordb_lucene.py patch
697
+ const serverParams: Record<string, unknown> = {
698
+ query: params.query,
699
+ };
700
+ if (params.limit !== undefined) {
701
+ serverParams.max_nodes = params.limit;
702
+ }
703
+ if (params.group_ids) {
704
+ serverParams.group_ids = params.group_ids;
705
+ }
706
+ if (params.entity_types) {
707
+ serverParams.entity_types = params.entity_types;
708
+ }
709
+ // Temporal filters (Madeinoz Patch)
710
+ if (params.since) {
711
+ serverParams.created_after = params.since;
712
+ }
713
+ if (params.until) {
714
+ serverParams.created_before = params.until;
715
+ }
716
+ // Weighted scoring (Feature 009)
717
+ if (params.include_weighted_scores !== undefined) {
718
+ serverParams.include_weighted_scores = params.include_weighted_scores;
719
+ }
720
+
721
+ // Check cache first
722
+ if (this.cache) {
723
+ const cacheKey = this.getCacheKey(MCP_TOOLS.SEARCH_NODES, serverParams);
724
+ const cached = this.cache.get(cacheKey);
725
+ if (cached) {
726
+ return { success: true, data: cached as unknown[] };
727
+ }
728
+
729
+ // Fetch and cache
730
+ const result = await this.callTool<unknown[]>(MCP_TOOLS.SEARCH_NODES, serverParams);
731
+ if (result.success && result.data) {
732
+ this.cache.set(cacheKey, result.data);
733
+ }
734
+ return result;
735
+ }
736
+
737
+ return await this.callTool<unknown[]>(MCP_TOOLS.SEARCH_NODES, serverParams);
738
+ }
739
+
740
+ /**
741
+ * Search for facts (relationships) in the knowledge graph
742
+ * Results are cached for repeated queries
743
+ *
744
+ * NOTE: Sanitization is handled server-side by falkordb_lucene.py patch
745
+ */
746
+ async searchFacts(params: SearchFactsParams): Promise<MCPClientResponse<unknown[]>> {
747
+ // Server-side sanitization handles Lucene escaping
748
+ const serverParams: Record<string, unknown> = {
749
+ query: params.query,
750
+ };
751
+ if (params.max_facts !== undefined) {
752
+ serverParams.max_facts = params.max_facts;
753
+ }
754
+ if (params.group_ids) {
755
+ serverParams.group_ids = params.group_ids;
756
+ }
757
+ if (params.center_node_uuid) {
758
+ serverParams.center_node_uuid = params.center_node_uuid;
759
+ }
760
+ // Temporal filters (Madeinoz Patch)
761
+ if (params.since) {
762
+ serverParams.created_after = params.since;
763
+ }
764
+ if (params.until) {
765
+ serverParams.created_before = params.until;
766
+ }
767
+
768
+ // Check cache first
769
+ if (this.cache) {
770
+ const cacheKey = this.getCacheKey(MCP_TOOLS.SEARCH_FACTS, serverParams);
771
+ const cached = this.cache.get(cacheKey);
772
+ if (cached) {
773
+ return { success: true, data: cached as unknown[] };
774
+ }
775
+
776
+ // Fetch and cache
777
+ const result = await this.callTool<unknown[]>(MCP_TOOLS.SEARCH_FACTS, serverParams);
778
+ if (result.success && result.data) {
779
+ this.cache.set(cacheKey, result.data);
780
+ }
781
+ return result;
782
+ }
783
+
784
+ return await this.callTool<unknown[]>(MCP_TOOLS.SEARCH_FACTS, serverParams);
785
+ }
786
+
787
+ /**
788
+ * Get recent episodes from the knowledge graph
789
+ *
790
+ * NOTE: Sanitization is handled server-side by falkordb_lucene.py patch
791
+ */
792
+ async getEpisodes(params: GetEpisodesParams = {}): Promise<MCPClientResponse<unknown[]>> {
793
+ // Build server params with correct field names
794
+ // Server-side sanitization handles Lucene escaping
795
+ const serverParams: Record<string, unknown> = {};
796
+ if (params.limit !== undefined) {
797
+ serverParams.max_episodes = params.limit;
798
+ }
799
+ // Support both group_id (single) and group_ids (multiple)
800
+ if (params.group_ids) {
801
+ serverParams.group_ids = params.group_ids;
802
+ } else if (params.group_id) {
803
+ // Server-side sanitization handles Lucene escaping
804
+ serverParams.group_ids = [params.group_id];
805
+ }
806
+ return await this.callTool<unknown[]>(MCP_TOOLS.GET_EPISODES, serverParams);
807
+ }
808
+
809
+ /**
810
+ * Get the status of the knowledge graph
811
+ */
812
+ async getStatus(): Promise<
813
+ MCPClientResponse<{
814
+ entity_count: number;
815
+ episode_count: number;
816
+ last_updated: string;
817
+ }>
818
+ > {
819
+ return await this.callTool<{
820
+ entity_count: number;
821
+ episode_count: number;
822
+ last_updated: string;
823
+ }>(MCP_TOOLS.GET_STATUS, {});
824
+ }
825
+
826
+ /**
827
+ * Clear all data from the knowledge graph
828
+ */
829
+ async clearGraph(): Promise<MCPClientResponse<{ success: boolean }>> {
830
+ return await this.callTool<{ success: boolean }>(MCP_TOOLS.CLEAR_GRAPH, {});
831
+ }
832
+
833
+ /**
834
+ * Delete an episode from the knowledge graph
835
+ */
836
+ async deleteEpisode(
837
+ params: DeleteEpisodeParams
838
+ ): Promise<MCPClientResponse<{ success: boolean }>> {
839
+ return await this.callTool<{ success: boolean }>(MCP_TOOLS.DELETE_EPISODE, params as unknown as Record<string, unknown>);
840
+ }
841
+
842
+ /**
843
+ * Delete an entity edge from the knowledge graph
844
+ */
845
+ async deleteEntityEdge(
846
+ params: DeleteEntityEdgeParams
847
+ ): Promise<MCPClientResponse<{ success: boolean }>> {
848
+ return await this.callTool<{ success: boolean }>(MCP_TOOLS.DELETE_ENTITY_EDGE, params as unknown as Record<string, unknown>);
849
+ }
850
+
851
+ /**
852
+ * Get an entity edge from the knowledge graph
853
+ */
854
+ async getEntityEdge(params: GetEntityEdgeParams): Promise<MCPClientResponse<unknown>> {
855
+ return await this.callTool<unknown>(MCP_TOOLS.GET_ENTITY_EDGE, params as unknown as Record<string, unknown>);
856
+ }
857
+
858
+ /**
859
+ * Feature 009: Get knowledge graph health metrics
860
+ */
861
+ async getKnowledgeHealth(
862
+ params: GetKnowledgeHealthParams = {}
863
+ ): Promise<MCPClientResponse<unknown>> {
864
+ const serverParams: Record<string, unknown> = {};
865
+ if (params.group_id) {
866
+ serverParams.group_id = params.group_id;
867
+ }
868
+ return await this.callTool<unknown>(MCP_TOOLS.GET_KNOWLEDGE_HEALTH, serverParams);
869
+ }
870
+
871
+ /**
872
+ * Feature 009: Run decay maintenance cycle
873
+ */
874
+ async runDecayMaintenance(
875
+ params: RunDecayMaintenanceParams = {}
876
+ ): Promise<MCPClientResponse<unknown>> {
877
+ const serverParams: Record<string, unknown> = {
878
+ dry_run: params.dry_run ?? false,
879
+ };
880
+ return await this.callTool<unknown>(MCP_TOOLS.RUN_DECAY_MAINTENANCE, serverParams);
881
+ }
882
+
883
+ /**
884
+ * Feature 009: Classify memory importance and stability
885
+ */
886
+ async classifyMemory(
887
+ params: ClassifyMemoryParams
888
+ ): Promise<MCPClientResponse<unknown>> {
889
+ const serverParams: Record<string, unknown> = {
890
+ content: params.content,
891
+ };
892
+ if (params.source_description) {
893
+ serverParams.source_description = params.source_description;
894
+ }
895
+ return await this.callTool<unknown>(MCP_TOOLS.CLASSIFY_MEMORY, serverParams);
896
+ }
897
+
898
+ /**
899
+ * Feature 009: Recover soft-deleted memory
900
+ */
901
+ async recoverSoftDeleted(
902
+ params: RecoverSoftDeletedParams
903
+ ): Promise<MCPClientResponse<unknown>> {
904
+ return await this.callTool<unknown>(MCP_TOOLS.RECOVER_SOFT_DELETED, {
905
+ uuid: params.uuid,
906
+ });
907
+ }
908
+
909
+ /**
910
+ * Feature 018: List ontology types (entity and relationship types)
911
+ */
912
+ async listOntologyTypes(): Promise<
913
+ MCPClientResponse<{
914
+ entity_types: string[];
915
+ relationship_types: string[];
916
+ entity_type_count: number;
917
+ relationship_type_count: number;
918
+ }>
919
+ > {
920
+ return await this.callTool<{
921
+ entity_types: string[];
922
+ relationship_types: string[];
923
+ entity_type_count: number;
924
+ relationship_type_count: number;
925
+ }>(MCP_TOOLS.LIST_ONTOLOGY_TYPES, {});
926
+ }
927
+
928
+ /**
929
+ * Feature 018: Validate ontology configuration
930
+ */
931
+ async validateOntology(): Promise<
932
+ MCPClientResponse<{
933
+ valid: boolean;
934
+ errors: string[];
935
+ warnings: string[];
936
+ breaking_changes: string[];
937
+ }>
938
+ > {
939
+ return await this.callTool<{
940
+ valid: boolean;
941
+ errors: string[];
942
+ warnings: string[];
943
+ breaking_changes: string[];
944
+ }>(MCP_TOOLS.VALIDATE_ONTOLOGY, {});
945
+ }
946
+
947
+ /**
948
+ * Feature 018: Reload ontology configuration
949
+ */
950
+ async reloadOntology(): Promise<
951
+ MCPClientResponse<{
952
+ success: boolean;
953
+ message: string;
954
+ entity_types: string[];
955
+ relationship_types: string[];
956
+ entity_type_count: number;
957
+ relationship_type_count: number;
958
+ version: string;
959
+ name: string;
960
+ breaking_changes: string[];
961
+ }>
962
+ > {
963
+ return await this.callTool<{
964
+ success: boolean;
965
+ message: string;
966
+ entity_types: string[];
967
+ relationship_types: string[];
968
+ entity_type_count: number;
969
+ relationship_type_count: number;
970
+ version: string;
971
+ name: string;
972
+ breaking_changes: string[];
973
+ }>(MCP_TOOLS.RELOAD_ONTOLOGY, {});
974
+ }
975
+
976
+ /**
977
+ * Feature 018: Import STIX 2.1 bundle
978
+ */
979
+ async importStixBundle(params: {
980
+ bundle_data: Record<string, unknown>;
981
+ batch_size?: number;
982
+ continue_on_error?: boolean;
983
+ }): Promise<
984
+ MCPClientResponse<{
985
+ session_id: string;
986
+ entities_imported: number;
987
+ relationships_imported: number;
988
+ errors: number;
989
+ warnings: string[];
990
+ }>
991
+ > {
992
+ return await this.callTool<{
993
+ session_id: string;
994
+ entities_imported: number;
995
+ relationships_imported: number;
996
+ errors: number;
997
+ warnings: string[];
998
+ }>(MCP_TOOLS.IMPORT_STIX_BUNDLE, params as unknown as Record<string, unknown>);
999
+ }
1000
+
1001
+ /**
1002
+ * Feature 018: Get STIX import status
1003
+ */
1004
+ /**
1005
+ * Feature 018: Get STIX import status
1006
+ */
1007
+ async getImportStatus(params: { import_id: string }): Promise<
1008
+ MCPClientResponse<{
1009
+ import_id: string;
1010
+ source_file: string;
1011
+ started_at: string;
1012
+ completed_at: string | null;
1013
+ status: string;
1014
+ total_objects: number;
1015
+ imported_count: number;
1016
+ failed_count: number;
1017
+ failed_object_ids: string[];
1018
+ error_messages: string[];
1019
+ }>
1020
+ > {
1021
+ return await this.callTool<{
1022
+ import_id: string;
1023
+ source_file: string;
1024
+ started_at: string;
1025
+ completed_at: string | null;
1026
+ status: string;
1027
+ total_objects: number;
1028
+ imported_count: number;
1029
+ failed_count: number;
1030
+ failed_object_ids: string[];
1031
+ error_messages: string[];
1032
+ }>(MCP_TOOLS.GET_IMPORT_STATUS, params as unknown as Record<string, unknown>);
1033
+ }
1034
+
1035
+ /**
1036
+ * Feature 020: Investigate an entity and return all connected relationships
1037
+ */
1038
+ async investigateEntity(
1039
+ params: InvestigateEntityParams
1040
+ ): Promise<
1041
+ MCPClientResponse<{
1042
+ entity: {
1043
+ uuid: string;
1044
+ name: string;
1045
+ labels: string[];
1046
+ summary?: string;
1047
+ created_at?: string;
1048
+ group_id?: string;
1049
+ };
1050
+ connections: Array<{
1051
+ relationship: string;
1052
+ direction: string;
1053
+ hop_distance: number;
1054
+ target_entity: {
1055
+ uuid: string;
1056
+ name: string;
1057
+ labels: string[];
1058
+ summary?: string;
1059
+ created_at?: string;
1060
+ group_id?: string;
1061
+ };
1062
+ fact?: string;
1063
+ confidence?: number;
1064
+ }>;
1065
+ metadata: {
1066
+ depth_explored: number;
1067
+ total_connections_explored: number;
1068
+ connections_returned: number;
1069
+ cycles_detected: number;
1070
+ cycles_pruned: number;
1071
+ entities_skipped: number;
1072
+ query_duration_ms: number;
1073
+ max_connections_exceeded?: boolean;
1074
+ };
1075
+ warning?: string;
1076
+ }>
1077
+ > {
1078
+ const serverParams: Record<string, unknown> = {
1079
+ entity_name: params.entity_name,
1080
+ };
1081
+ if (params.max_depth !== undefined) {
1082
+ serverParams.max_depth = params.max_depth;
1083
+ }
1084
+ if (params.relationship_types) {
1085
+ serverParams.relationship_types = params.relationship_types;
1086
+ }
1087
+ if (params.group_ids) {
1088
+ serverParams.group_ids = params.group_ids;
1089
+ }
1090
+ if (params.include_attributes !== undefined) {
1091
+ serverParams.include_attributes = params.include_attributes;
1092
+ }
1093
+ return await this.callTool(MCP_TOOLS.INVESTIGATE_ENTITY, serverParams);
1094
+ }
1095
+
1096
+ /**
1097
+ * Test the connection to the MCP server
1098
+ */
1099
+ async testConnection(): Promise<MCPClientResponse<{ status: string }>> {
1100
+ // Test mode: return mock response without connecting
1101
+ // Used by integration tests to avoid network calls
1102
+ if (process.env.MADEINOZ_KNOWLEDGE_TEST_MODE === 'true') {
1103
+ return {
1104
+ success: true,
1105
+ data: { status: 'ok' },
1106
+ };
1107
+ }
1108
+
1109
+ try {
1110
+ const controller = new AbortController();
1111
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout for health check
1112
+
1113
+ // T021 [P] [US2]: Apply TLS configuration for HTTPS requests
1114
+ const tlsOptions = createHTTPSOptions(this.tlsConfig);
1115
+
1116
+ const response = await fetch(`${this.baseURL.replace(/\/mcp\/?$/, '')}/health`, {
1117
+ method: 'GET',
1118
+ signal: controller.signal,
1119
+ ...tlsOptions,
1120
+ });
1121
+
1122
+ clearTimeout(timeoutId);
1123
+
1124
+ if (!response.ok) {
1125
+ return {
1126
+ success: false,
1127
+ error: `HTTP ${response.status}: ${response.statusText}`,
1128
+ };
1129
+ }
1130
+
1131
+ const data = await response.json();
1132
+ return {
1133
+ success: true,
1134
+ data: data as { status: string },
1135
+ };
1136
+ } catch (error: unknown) {
1137
+ if (error instanceof Error) {
1138
+ // T018 [US1]: Implement connection error handling with actionable messages
1139
+ // T026 [US2]: Add TLS certificate error handling with clear messages
1140
+ const errorMsg = error.message.toLowerCase();
1141
+ const suggestions: string[] = [];
1142
+
1143
+ // TLS/SSL certificate errors
1144
+ if (
1145
+ errorMsg.includes('certificate') ||
1146
+ errorMsg.includes('tls') ||
1147
+ errorMsg.includes('ssl') ||
1148
+ errorMsg.includes('handshake')
1149
+ ) {
1150
+ suggestions.push('Check that the server certificate is valid');
1151
+ suggestions.push('Try MADEINOZ_KNOWLEDGE_TLS_VERIFY=false for self-signed certificates (not recommended for production)');
1152
+ if (this.tlsConfig?.ca) {
1153
+ suggestions.push(`Verify CA certificate path exists: ${this.tlsConfig.ca}`);
1154
+ }
1155
+ return {
1156
+ success: false,
1157
+ error: `TLS Certificate Error: ${error.message}\nSuggestions:\n - ${suggestions.join('\n - ')}`,
1158
+ };
1159
+ }
1160
+
1161
+ // Host unreachable / DNS resolution errors
1162
+ if (
1163
+ errorMsg.includes('econnrefused') ||
1164
+ errorMsg.includes('connection refused') ||
1165
+ errorMsg.includes('econnreset') ||
1166
+ errorMsg.includes('enotfound') ||
1167
+ errorMsg.includes('getaddrinfo')
1168
+ ) {
1169
+ suggestions.push('Verify the MCP server is running (bun run server status)');
1170
+ suggestions.push('Check firewall settings allow connections');
1171
+ suggestions.push('Verify the host and port are correct');
1172
+ if (this.baseURL.includes('localhost') || this.baseURL.includes('127.0.0.1')) {
1173
+ suggestions.push('If running in Docker, ensure ports are properly mapped');
1174
+ }
1175
+ return {
1176
+ success: false,
1177
+ error: `Connection Error: Unable to reach server at ${this.baseURL}\nSuggestions:\n - ${suggestions.join('\n - ')}`,
1178
+ };
1179
+ }
1180
+
1181
+ // Network timeout errors
1182
+ if (
1183
+ errorMsg.includes('timeout') ||
1184
+ errorMsg.includes('timed out') ||
1185
+ error.name === 'AbortError'
1186
+ ) {
1187
+ suggestions.push('The request took too long to complete');
1188
+ suggestions.push('Check if the server is under heavy load');
1189
+ suggestions.push('Try increasing MADEINOZ_KNOWLEDGE_TIMEOUT (currently ${this.timeout}ms)');
1190
+ return {
1191
+ success: false,
1192
+ error: `Connection Timeout: ${error.message}\nSuggestions:\n - ${suggestions.join('\n - ')}`,
1193
+ };
1194
+ }
1195
+
1196
+ // Invalid protocol errors
1197
+ if (errorMsg.includes('invalid protocol') || errorMsg.includes('unsupported protocol')) {
1198
+ suggestions.push('Check MADEINOZ_KNOWLEDGE_PROTOCOL is "http" or "https"');
1199
+ return {
1200
+ success: false,
1201
+ error: `Protocol Error: ${error.message}\nSuggestions:\n - ${suggestions.join('\n - ')}`,
1202
+ };
1203
+ }
1204
+
1205
+ // Generic error with message
1206
+ return {
1207
+ success: false,
1208
+ error: error.message,
1209
+ };
1210
+ }
1211
+ return {
1212
+ success: false,
1213
+ error: 'Unknown error occurred',
1214
+ };
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ /**
1220
+ * Create an MCP client instance with configuration from environment variables or profiles
1221
+ *
1222
+ * T015 [US1]: Update createMCPClient() to accept extended config
1223
+ * T016 [US1]: Add environment variable parsing
1224
+ * T023 [US2]: Add MADEINOZ_KNOWLEDGE_TLS_VERIFY environment variable support
1225
+ * T024 [US2]: Add MADEINOZ_KNOWLEDGE_TLS_CA environment variable support
1226
+ *
1227
+ * Priority order (highest to lowest):
1228
+ * 1. Explicit config parameter
1229
+ * 2. Individual environment variables (MADEINOZ_KNOWLEDGE_HOST, etc.)
1230
+ * 3. Profile from MADEINOZ_KNOWLEDGE_PROFILE environment variable
1231
+ * 4. Default profile from YAML file
1232
+ * 5. Code defaults (localhost:8001, http)
1233
+ *
1234
+ * Environment variables (MADEINOZ_KNOWLEDGE_* prefix):
1235
+ * - MADEINOZ_KNOWLEDGE_PROFILE: Profile name to load from YAML file
1236
+ * - MADEINOZ_KNOWLEDGE_PROTOCOL: http or https (default: http)
1237
+ * - MADEINOZ_KNOWLEDGE_HOST: hostname or IP address (default: localhost)
1238
+ * - MADEINOZ_KNOWLEDGE_PORT: TCP port (default: 8001)
1239
+ * - MADEINOZ_KNOWLEDGE_BASE_PATH: URL path prefix (default: /mcp)
1240
+ * - MADEINOZ_KNOWLEDGE_TLS_VERIFY: true or false (default: true)
1241
+ * - MADEINOZ_KNOWLEDGE_TLS_CA: Path to CA certificate file
1242
+ * - MADEINOZ_KNOWLEDGE_TLS_CERT: Path to client certificate file
1243
+ * - MADEINOZ_KNOWLEDGE_TLS_KEY: Path to client private key file
1244
+ */
1245
+ export function createMCPClient(config?: MCPClientConfig): MCPClient {
1246
+ // If explicit config provided with all necessary fields, use it directly
1247
+ if (config && (config.baseURL || (config.host && config.port))) {
1248
+ return new MCPClient(config);
1249
+ }
1250
+
1251
+ // Build extended config from environment variables
1252
+ const envConfig: MCPClientConfigExtended = {
1253
+ protocol: (process.env.MADEINOZ_KNOWLEDGE_PROTOCOL as 'http' | 'https') || undefined,
1254
+ host: process.env.MADEINOZ_KNOWLEDGE_HOST || undefined,
1255
+ port: process.env.MADEINOZ_KNOWLEDGE_PORT ? Number.parseInt(process.env.MADEINOZ_KNOWLEDGE_PORT, 10) : undefined,
1256
+ basePath: process.env.MADEINOZ_KNOWLEDGE_BASE_PATH || undefined,
1257
+ tls: {
1258
+ // T023 [US2]: Add MADEINOZ_KNOWLEDGE_TLS_VERIFY environment variable support
1259
+ verify: process.env.MADEINOZ_KNOWLEDGE_TLS_VERIFY ? process.env.MADEINOZ_KNOWLEDGE_TLS_VERIFY !== 'false' : undefined,
1260
+ // T024 [US2]: Add MADEINOZ_KNOWLEDGE_TLS_CA environment variable support
1261
+ ca: process.env.MADEINOZ_KNOWLEDGE_TLS_CA || undefined,
1262
+ cert: process.env.MADEINOZ_KNOWLEDGE_TLS_CERT || undefined,
1263
+ key: process.env.MADEINOZ_KNOWLEDGE_TLS_KEY || undefined,
1264
+ },
1265
+ ...config,
1266
+ };
1267
+
1268
+ // Remove undefined TLS config to avoid overriding defaults
1269
+ if (!envConfig.tls?.ca && !envConfig.tls?.cert && !envConfig.tls?.key && envConfig.tls?.verify === undefined) {
1270
+ delete envConfig.tls;
1271
+ }
1272
+
1273
+ // If no explicit config and no environment variables, try loading from profile
1274
+ if (!config && !process.env.MADEINOZ_KNOWLEDGE_HOST && !process.env.MADEINOZ_KNOWLEDGE_PORT) {
1275
+ try {
1276
+ // Load profile from config file (imported at top of file)
1277
+ const profileConfig = loadProfileWithOverrides();
1278
+
1279
+ // Convert profile config to MCPClientConfig format
1280
+ const profileBasedConfig: MCPClientConfigExtended = {
1281
+ protocol: profileConfig.protocol as 'http' | 'https',
1282
+ host: profileConfig.host,
1283
+ port: profileConfig.port,
1284
+ basePath: profileConfig.basePath,
1285
+ timeout: profileConfig.timeout,
1286
+ tls: profileConfig.tls,
1287
+ profile: profileConfig.name,
1288
+ };
1289
+
1290
+ // Profile settings as base, environment variables override (only defined values)
1291
+ // Note: envConfig must come first so undefined values don't override profile values
1292
+ const finalConfig = { ...envConfig, ...profileBasedConfig };
1293
+ return new MCPClient(finalConfig);
1294
+ } catch (_error) {
1295
+ // If profile loading fails, fall back to environment config or defaults
1296
+ // This ensures backward compatibility
1297
+ }
1298
+ }
1299
+
1300
+ return new MCPClient(envConfig);
1301
+ }
1302
+
1303
+ /**
1304
+ * Quick health check function
1305
+ */
1306
+ export async function checkHealth(baseURL?: string): Promise<boolean> {
1307
+ const client = new MCPClient({ baseURL });
1308
+ const result = await client.testConnection();
1309
+ return result.success;
1310
+ }