@juspay/neurolink 9.51.3 → 9.51.4

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 (47) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/artifacts/artifactStore.d.ts +56 -0
  3. package/dist/artifacts/artifactStore.js +143 -0
  4. package/dist/browser/neurolink.min.js +307 -296
  5. package/dist/cli/commands/mcp.d.ts +6 -0
  6. package/dist/cli/commands/mcp.js +128 -86
  7. package/dist/core/redisConversationMemoryManager.js +20 -0
  8. package/dist/lib/artifacts/artifactStore.d.ts +56 -0
  9. package/dist/lib/artifacts/artifactStore.js +144 -0
  10. package/dist/lib/core/redisConversationMemoryManager.js +20 -0
  11. package/dist/lib/mcp/externalServerManager.d.ts +6 -0
  12. package/dist/lib/mcp/externalServerManager.js +9 -0
  13. package/dist/lib/mcp/mcpOutputNormalizer.d.ts +49 -0
  14. package/dist/lib/mcp/mcpOutputNormalizer.js +182 -0
  15. package/dist/lib/mcp/toolDiscoveryService.d.ts +10 -0
  16. package/dist/lib/mcp/toolDiscoveryService.js +32 -1
  17. package/dist/lib/memory/memoryRetrievalTools.d.ts +64 -9
  18. package/dist/lib/memory/memoryRetrievalTools.js +77 -9
  19. package/dist/lib/neurolink.d.ts +2 -0
  20. package/dist/lib/neurolink.js +59 -80
  21. package/dist/lib/session/globalSessionState.js +44 -1
  22. package/dist/lib/types/artifactTypes.d.ts +63 -0
  23. package/dist/lib/types/artifactTypes.js +11 -0
  24. package/dist/lib/types/configTypes.d.ts +32 -0
  25. package/dist/lib/types/conversation.d.ts +7 -0
  26. package/dist/lib/types/index.d.ts +2 -0
  27. package/dist/lib/types/mcpOutputTypes.d.ts +40 -0
  28. package/dist/lib/types/mcpOutputTypes.js +9 -0
  29. package/dist/mcp/externalServerManager.d.ts +6 -0
  30. package/dist/mcp/externalServerManager.js +9 -0
  31. package/dist/mcp/mcpOutputNormalizer.d.ts +49 -0
  32. package/dist/mcp/mcpOutputNormalizer.js +181 -0
  33. package/dist/mcp/toolDiscoveryService.d.ts +10 -0
  34. package/dist/mcp/toolDiscoveryService.js +32 -1
  35. package/dist/memory/memoryRetrievalTools.d.ts +64 -9
  36. package/dist/memory/memoryRetrievalTools.js +77 -9
  37. package/dist/neurolink.d.ts +2 -0
  38. package/dist/neurolink.js +59 -80
  39. package/dist/session/globalSessionState.js +44 -1
  40. package/dist/types/artifactTypes.d.ts +63 -0
  41. package/dist/types/artifactTypes.js +10 -0
  42. package/dist/types/configTypes.d.ts +32 -0
  43. package/dist/types/conversation.d.ts +7 -0
  44. package/dist/types/index.d.ts +2 -0
  45. package/dist/types/mcpOutputTypes.d.ts +40 -0
  46. package/dist/types/mcpOutputTypes.js +8 -0
  47. package/package.json +1 -1
@@ -67,6 +67,12 @@ export declare class MCPCommandFactory {
67
67
  * Execute exec command
68
68
  */
69
69
  private static executeExec;
70
+ /**
71
+ * Run the actual MCP tool execution and format/write the output.
72
+ * Extracted to keep executeExec nesting within ESLint max-depth limit.
73
+ * Returns 0 on success, 1 on failure.
74
+ */
75
+ private static runExecTool;
70
76
  /**
71
77
  * Execute remove command
72
78
  */
@@ -656,104 +656,146 @@ export class MCPCommandFactory {
656
656
  }
657
657
  }
658
658
  const sdk = new NeuroLink();
659
- await this.getMCPStatusWithTimeout(sdk);
660
- // Check if server exists and is connected
661
- const allServers = await sdk.listMCPServers();
662
- const server = allServers.find((s) => s.name === serverName);
663
- if (!server) {
664
- if (spinner) {
665
- spinner.fail();
666
- }
667
- logger.error(chalk.red(`❌ Server not found: ${serverName}`));
668
- process.exit(1);
669
- }
670
- if (server.status !== "connected") {
671
- if (spinner) {
672
- spinner.fail();
673
- }
674
- logger.error(chalk.red(`❌ Server not connected: ${serverName}`));
675
- logger.always(chalk.yellow("💡 Try: neurolink mcp test " + serverName));
676
- process.exit(1);
677
- }
678
- // Check if tool exists
679
- const tool = server.tools?.find((t) => t.name === toolName);
680
- if (!tool) {
681
- if (spinner) {
682
- spinner.fail();
683
- }
684
- logger.error(chalk.red(`❌ Tool not found: ${toolName}`));
685
- if (server.tools?.length) {
686
- logger.always(chalk.blue("Available tools:"));
687
- server.tools.forEach((t) => {
688
- logger.always(` • ${t.name}: ${t.description}`);
689
- });
690
- }
691
- process.exit(1);
692
- }
693
- // Execute the tool using the NeuroLink MCP tool registry
659
+ let exitCode = 0;
694
660
  try {
695
- const { toolRegistry } = await import("../../lib/mcp/toolRegistry.js");
696
- const executionResult = await toolRegistry.executeTool(toolName, params, {
697
- sessionId: `cli-${Date.now()}`,
698
- userId: process.env.USER || "cli-user",
699
- config: {
700
- domainType: "cli-execution",
701
- customData: { serverName },
702
- },
703
- });
704
- const result = {
705
- tool: toolName,
706
- server: serverName,
707
- params,
708
- result: executionResult,
709
- success: true,
710
- timestamp: new Date().toISOString(),
711
- };
712
- if (spinner) {
713
- spinner.succeed(chalk.green("✅ Tool executed successfully"));
714
- }
715
- // Display results
716
- if (argv.format === "json") {
717
- logger.always(JSON.stringify(result, null, 2));
718
- }
719
- else {
720
- logger.always(chalk.green("🔧 Tool Execution Results:"));
721
- logger.always(` Tool: ${chalk.cyan(toolName)}`);
722
- logger.always(` Server: ${chalk.cyan(serverName)}`);
723
- logger.always(` Result: ${JSON.stringify(executionResult, null, 2)}`);
724
- logger.always(` Timestamp: ${result.timestamp}`);
725
- }
726
- }
727
- catch (toolError) {
728
- const errorMessage = toolError instanceof Error ? toolError.message : String(toolError);
729
- if (spinner) {
730
- spinner.fail(chalk.red("❌ Tool execution failed"));
661
+ await this.getMCPStatusWithTimeout(sdk);
662
+ // Check if server exists and is connected
663
+ const allServers = await sdk.listMCPServers();
664
+ const server = allServers.find((s) => s.name === serverName);
665
+ if (!server) {
666
+ if (spinner) {
667
+ spinner.fail();
668
+ }
669
+ logger.error(chalk.red(`❌ Server not found: ${serverName}`));
670
+ exitCode = 1;
731
671
  }
732
- const result = {
733
- tool: toolName,
734
- server: serverName,
735
- params,
736
- _error: errorMessage,
737
- success: false,
738
- timestamp: new Date().toISOString(),
739
- };
740
- if (argv.format === "json") {
741
- logger.always(JSON.stringify(result, null, 2));
672
+ else if (server.status !== "connected") {
673
+ if (spinner) {
674
+ spinner.fail();
675
+ }
676
+ logger.error(chalk.red(`❌ Server not connected: ${serverName}`));
677
+ logger.always(chalk.yellow("💡 Try: neurolink mcp test " + serverName));
678
+ exitCode = 1;
742
679
  }
743
680
  else {
744
- logger.error(chalk.red("🔧 Tool Execution Failed:"));
745
- logger.error(` Tool: ${chalk.cyan(toolName)}`);
746
- logger.error(` Server: ${chalk.cyan(serverName)}`);
747
- logger.error(` Error: ${chalk.red(errorMessage)}`);
681
+ // Check if tool exists
682
+ const tool = server.tools?.find((t) => t.name === toolName);
683
+ if (!tool) {
684
+ if (spinner) {
685
+ spinner.fail();
686
+ }
687
+ logger.error(chalk.red(`❌ Tool not found: ${toolName}`));
688
+ if (server.tools?.length) {
689
+ logger.always(chalk.blue("Available tools:"));
690
+ server.tools.forEach((t) => {
691
+ logger.always(` • ${t.name}: ${t.description}`);
692
+ });
693
+ }
694
+ exitCode = 1;
695
+ }
696
+ else {
697
+ exitCode = await this.runExecTool(toolName, serverName, params, argv, spinner);
698
+ }
748
699
  }
749
- process.exit(1);
750
700
  }
701
+ finally {
702
+ // Always shut down MCP connections to prevent lingering child processes
703
+ await sdk.shutdown().catch(() => undefined);
704
+ }
705
+ // Flush stdout/stderr before exit so buffered output (spinner lines,
706
+ // result text) is not truncated by process.exit(). process.exit() is
707
+ // still required because MCP stdio connections and health-check timers
708
+ // keep the Node.js event loop alive even after sdk.shutdown().
709
+ await new Promise((resolve) => {
710
+ process.stdout.write("", () => {
711
+ process.stderr.write("", () => resolve());
712
+ });
713
+ });
714
+ process.exit(exitCode);
751
715
  }
752
716
  catch (_error) {
753
717
  logger.error(chalk.red(`❌ Exec command failed: ${_error.message}`));
754
718
  process.exit(1);
755
719
  }
756
720
  }
721
+ /**
722
+ * Run the actual MCP tool execution and format/write the output.
723
+ * Extracted to keep executeExec nesting within ESLint max-depth limit.
724
+ * Returns 0 on success, 1 on failure.
725
+ */
726
+ static async runExecTool(toolName, serverName, params, argv, spinner) {
727
+ const WARN_BYTES = 50 * 1024; // 50 KB
728
+ try {
729
+ const { toolRegistry } = await import("../../lib/mcp/toolRegistry.js");
730
+ const executionResult = await withTimeout(toolRegistry.executeTool(toolName, params, {
731
+ sessionId: `cli-${Date.now()}`,
732
+ userId: process.env.USER || "cli-user",
733
+ config: { domainType: "cli-execution", customData: { serverName } },
734
+ }), MCP_STATUS_TIMEOUT_MS, ErrorFactory.toolTimeout(toolName, MCP_STATUS_TIMEOUT_MS));
735
+ const result = {
736
+ tool: toolName,
737
+ server: serverName,
738
+ params,
739
+ result: executionResult,
740
+ success: true,
741
+ timestamp: new Date().toISOString(),
742
+ };
743
+ if (spinner) {
744
+ spinner.succeed(chalk.green("✅ Tool executed successfully"));
745
+ }
746
+ const resultJson = JSON.stringify(result, null, 2);
747
+ const resultBytes = Buffer.byteLength(resultJson, "utf-8");
748
+ const outputFile = argv.output;
749
+ if (outputFile) {
750
+ await fs.promises.writeFile(outputFile, resultJson, "utf-8");
751
+ logger.always(chalk.green(`✅ Result saved to: ${chalk.cyan(outputFile)}`));
752
+ logger.always(` Size: ${resultBytes >= 1024 ? `${(resultBytes / 1024).toFixed(1)} KB` : `${resultBytes} B`}`);
753
+ }
754
+ else if (argv.format === "json") {
755
+ if (resultBytes > WARN_BYTES) {
756
+ logger.warn(chalk.yellow(`⚠ Large result (${(resultBytes / 1024).toFixed(1)} KB). ` +
757
+ `Use --output <file> to save to disk instead.`));
758
+ }
759
+ logger.always(resultJson);
760
+ }
761
+ else {
762
+ if (resultBytes > WARN_BYTES) {
763
+ logger.warn(chalk.yellow(`⚠ Large result (${(resultBytes / 1024).toFixed(1)} KB). ` +
764
+ `Use --output <file> to save to disk instead.`));
765
+ }
766
+ logger.always(chalk.green("🔧 Tool Execution Results:"));
767
+ logger.always(` Tool: ${chalk.cyan(toolName)}`);
768
+ logger.always(` Server: ${chalk.cyan(serverName)}`);
769
+ logger.always(` Result: ${JSON.stringify(executionResult, null, 2)}`);
770
+ logger.always(` Timestamp: ${result.timestamp}`);
771
+ }
772
+ return 0;
773
+ }
774
+ catch (toolError) {
775
+ const errorMessage = toolError instanceof Error ? toolError.message : String(toolError);
776
+ if (spinner) {
777
+ spinner.fail(chalk.red("❌ Tool execution failed"));
778
+ }
779
+ const result = {
780
+ tool: toolName,
781
+ server: serverName,
782
+ params,
783
+ _error: errorMessage,
784
+ success: false,
785
+ timestamp: new Date().toISOString(),
786
+ };
787
+ if (argv.format === "json") {
788
+ logger.always(JSON.stringify(result, null, 2));
789
+ }
790
+ else {
791
+ logger.error(chalk.red("🔧 Tool Execution Failed:"));
792
+ logger.error(` Tool: ${chalk.cyan(toolName)}`);
793
+ logger.error(` Server: ${chalk.cyan(serverName)}`);
794
+ logger.error(` Error: ${chalk.red(errorMessage)}`);
795
+ }
796
+ return 1;
797
+ }
798
+ }
757
799
  /**
758
800
  * Execute remove command
759
801
  */
@@ -7,6 +7,7 @@ import { tracers } from "../telemetry/tracers.js";
7
7
  import { randomUUID } from "crypto";
8
8
  import { MESSAGES_PER_TURN } from "../config/conversationMemory.js";
9
9
  import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
10
+ import { NEUROLINK_ARTIFACT_ID_KEY } from "../mcp/mcpOutputNormalizer.js";
10
11
  import { SummarizationEngine } from "../context/summarizationEngine.js";
11
12
  import { NeuroLink } from "../neurolink.js";
12
13
  import { ConversationMemoryError } from "../types/conversation.js";
@@ -1320,11 +1321,30 @@ User message: "${userMessage}"`;
1320
1321
  maxBytes: this.config?.contextCompaction?.maxToolOutputBytes,
1321
1322
  maxLines: this.config?.contextCompaction?.maxToolOutputLines,
1322
1323
  });
1324
+ // Extract artifact ID if this result was externalized by McpOutputNormalizer.
1325
+ // The surrogate carries `_meta.neurolinkArtifactId` on the raw result object.
1326
+ let artifactId;
1327
+ try {
1328
+ const rawResult = toolResult.result;
1329
+ if (rawResult && typeof rawResult === "object") {
1330
+ const meta = rawResult._meta;
1331
+ if (meta && typeof meta === "object") {
1332
+ const idValue = meta[NEUROLINK_ARTIFACT_ID_KEY];
1333
+ if (typeof idValue === "string") {
1334
+ artifactId = idValue;
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ catch {
1340
+ // Ignore extraction errors — artifact ID is best-effort metadata
1341
+ }
1323
1342
  // Build metadata — only store preview when truncation occurred (no duplication)
1324
1343
  const metadata = {
1325
1344
  truncated,
1326
1345
  ...(truncated && { toolOutputPreview: preview }),
1327
1346
  ...(truncated && { originalSize }),
1347
+ ...(artifactId && { artifactId }),
1328
1348
  };
1329
1349
  // Build result — success/error metadata only, NOT the output data
1330
1350
  const result = {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Artifact Store
3
+ *
4
+ * Pluggable storage for externalized MCP tool outputs.
5
+ *
6
+ * When `mcp.outputLimits.strategy = "externalize"` the full tool payload is
7
+ * written here instead of being sent inline to the LLM. The model receives a
8
+ * compact surrogate with a preview and an artifact ID. The full payload can be
9
+ * retrieved on demand via the `retrieve_context` tool.
10
+ *
11
+ * Architecture:
12
+ * ArtifactStore (interface) — canonical types in src/lib/types/artifactTypes.ts
13
+ * LocalTempArtifactStore — single-process, filesystem-backed implementation
14
+ *
15
+ * Distributed backends (S3, Redis blobs) can be added later by implementing
16
+ * ArtifactStore from types/artifactTypes.ts.
17
+ *
18
+ * @module artifacts/artifactStore
19
+ */
20
+ import type { ArtifactMeta, ArtifactRef, ArtifactStore } from "../types/artifactTypes.js";
21
+ export type { ArtifactMeta, ArtifactRef, ArtifactStore, } from "../types/artifactTypes.js";
22
+ /**
23
+ * Filesystem-backed artifact store using the OS temp directory.
24
+ *
25
+ * Files are written with mode 0o600 (owner read/write only).
26
+ * An in-memory index tracks metadata without a separate index file.
27
+ *
28
+ * Suitable for:
29
+ * - CLI usage
30
+ * - Single-process SDK deployments
31
+ * - Multi-process deployments where each process manages its own artifacts
32
+ * (artifacts created in one process are not visible to others)
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const store = new LocalTempArtifactStore();
37
+ * const ref = await store.store(largeJson, {
38
+ * toolName: "list_files",
39
+ * serverId: "filesystem-server",
40
+ * sizeBytes: Buffer.byteLength(largeJson),
41
+ * contentType: "json",
42
+ * });
43
+ * // Later, via retrieve_context:
44
+ * const full = await store.retrieve(ref.id);
45
+ * ```
46
+ */
47
+ export declare class LocalTempArtifactStore implements ArtifactStore {
48
+ private readonly dir;
49
+ private readonly index;
50
+ constructor(dir?: string);
51
+ generatePreview(payload: string): string;
52
+ store(payload: string, meta: Omit<ArtifactMeta, "createdAt">): Promise<ArtifactRef>;
53
+ retrieve(id: string): Promise<string | null>;
54
+ delete(id: string): Promise<void>;
55
+ cleanup(olderThanMs: number): Promise<number>;
56
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Artifact Store
3
+ *
4
+ * Pluggable storage for externalized MCP tool outputs.
5
+ *
6
+ * When `mcp.outputLimits.strategy = "externalize"` the full tool payload is
7
+ * written here instead of being sent inline to the LLM. The model receives a
8
+ * compact surrogate with a preview and an artifact ID. The full payload can be
9
+ * retrieved on demand via the `retrieve_context` tool.
10
+ *
11
+ * Architecture:
12
+ * ArtifactStore (interface) — canonical types in src/lib/types/artifactTypes.ts
13
+ * LocalTempArtifactStore — single-process, filesystem-backed implementation
14
+ *
15
+ * Distributed backends (S3, Redis blobs) can be added later by implementing
16
+ * ArtifactStore from types/artifactTypes.ts.
17
+ *
18
+ * @module artifacts/artifactStore
19
+ */
20
+ import { randomUUID } from "node:crypto";
21
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { logger } from "../utils/logger.js";
25
+ // ---------------------------------------------------------------------------
26
+ // LocalTempArtifactStore
27
+ // ---------------------------------------------------------------------------
28
+ /** Characters used for the quick preview embedded in surrogate results. */
29
+ const DEFAULT_PREVIEW_CHARS = 500;
30
+ /**
31
+ * Filesystem-backed artifact store using the OS temp directory.
32
+ *
33
+ * Files are written with mode 0o600 (owner read/write only).
34
+ * An in-memory index tracks metadata without a separate index file.
35
+ *
36
+ * Suitable for:
37
+ * - CLI usage
38
+ * - Single-process SDK deployments
39
+ * - Multi-process deployments where each process manages its own artifacts
40
+ * (artifacts created in one process are not visible to others)
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const store = new LocalTempArtifactStore();
45
+ * const ref = await store.store(largeJson, {
46
+ * toolName: "list_files",
47
+ * serverId: "filesystem-server",
48
+ * sizeBytes: Buffer.byteLength(largeJson),
49
+ * contentType: "json",
50
+ * });
51
+ * // Later, via retrieve_context:
52
+ * const full = await store.retrieve(ref.id);
53
+ * ```
54
+ */
55
+ export class LocalTempArtifactStore {
56
+ dir;
57
+ index = new Map();
58
+ constructor(dir) {
59
+ this.dir = dir ?? join(tmpdir(), "neurolink-artifacts");
60
+ }
61
+ generatePreview(payload) {
62
+ if (payload.length <= DEFAULT_PREVIEW_CHARS) {
63
+ return payload;
64
+ }
65
+ return `${payload.slice(0, DEFAULT_PREVIEW_CHARS)}…`;
66
+ }
67
+ async store(payload, meta) {
68
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
69
+ const id = randomUUID();
70
+ const ext = meta.contentType === "json" ? ".json" : ".txt";
71
+ const filePath = join(this.dir, `${id}${ext}`);
72
+ await writeFile(filePath, payload, { encoding: "utf-8", mode: 0o600 });
73
+ const fullMeta = {
74
+ ...meta,
75
+ createdAt: Date.now(),
76
+ path: filePath,
77
+ };
78
+ this.index.set(id, fullMeta);
79
+ logger.debug(`[ArtifactStore] Stored artifact ${id} for tool "${meta.toolName}" ` +
80
+ `(${formatBytes(meta.sizeBytes)})`);
81
+ return {
82
+ id,
83
+ preview: this.generatePreview(payload),
84
+ sizeBytes: meta.sizeBytes,
85
+ meta: { ...meta, createdAt: fullMeta.createdAt },
86
+ };
87
+ }
88
+ async retrieve(id) {
89
+ const entry = this.index.get(id);
90
+ if (!entry) {
91
+ logger.debug(`[ArtifactStore] Artifact ${id} not in index`);
92
+ return null;
93
+ }
94
+ try {
95
+ const content = await readFile(entry.path, "utf-8");
96
+ logger.debug(`[ArtifactStore] Retrieved artifact ${id} (${formatBytes(entry.sizeBytes)})`);
97
+ return content;
98
+ }
99
+ catch (err) {
100
+ logger.warn(`[ArtifactStore] Failed to read artifact ${id}: ${err instanceof Error ? err.message : String(err)}`);
101
+ return null;
102
+ }
103
+ }
104
+ async delete(id) {
105
+ const entry = this.index.get(id);
106
+ if (!entry) {
107
+ return;
108
+ }
109
+ try {
110
+ await rm(entry.path, { force: true });
111
+ }
112
+ catch {
113
+ // Suppress — file may already be gone
114
+ }
115
+ this.index.delete(id);
116
+ }
117
+ async cleanup(olderThanMs) {
118
+ const cutoff = Date.now() - olderThanMs;
119
+ let count = 0;
120
+ for (const [id, entry] of this.index.entries()) {
121
+ if (entry.createdAt < cutoff) {
122
+ await this.delete(id);
123
+ count++;
124
+ }
125
+ }
126
+ if (count > 0) {
127
+ logger.debug(`[ArtifactStore] Cleaned up ${count} expired artifact(s)`);
128
+ }
129
+ return count;
130
+ }
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // Helpers
134
+ // ---------------------------------------------------------------------------
135
+ function formatBytes(bytes) {
136
+ if (bytes < 1024) {
137
+ return `${bytes} B`;
138
+ }
139
+ if (bytes < 1024 * 1024) {
140
+ return `${(bytes / 1024).toFixed(1)} KB`;
141
+ }
142
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
143
+ }
144
+ //# sourceMappingURL=artifactStore.js.map
@@ -7,6 +7,7 @@ import { tracers } from "../telemetry/tracers.js";
7
7
  import { randomUUID } from "crypto";
8
8
  import { MESSAGES_PER_TURN } from "../config/conversationMemory.js";
9
9
  import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
10
+ import { NEUROLINK_ARTIFACT_ID_KEY } from "../mcp/mcpOutputNormalizer.js";
10
11
  import { SummarizationEngine } from "../context/summarizationEngine.js";
11
12
  import { NeuroLink } from "../neurolink.js";
12
13
  import { ConversationMemoryError } from "../types/conversation.js";
@@ -1320,11 +1321,30 @@ User message: "${userMessage}"`;
1320
1321
  maxBytes: this.config?.contextCompaction?.maxToolOutputBytes,
1321
1322
  maxLines: this.config?.contextCompaction?.maxToolOutputLines,
1322
1323
  });
1324
+ // Extract artifact ID if this result was externalized by McpOutputNormalizer.
1325
+ // The surrogate carries `_meta.neurolinkArtifactId` on the raw result object.
1326
+ let artifactId;
1327
+ try {
1328
+ const rawResult = toolResult.result;
1329
+ if (rawResult && typeof rawResult === "object") {
1330
+ const meta = rawResult._meta;
1331
+ if (meta && typeof meta === "object") {
1332
+ const idValue = meta[NEUROLINK_ARTIFACT_ID_KEY];
1333
+ if (typeof idValue === "string") {
1334
+ artifactId = idValue;
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ catch {
1340
+ // Ignore extraction errors — artifact ID is best-effort metadata
1341
+ }
1323
1342
  // Build metadata — only store preview when truncation occurred (no duplication)
1324
1343
  const metadata = {
1325
1344
  truncated,
1326
1345
  ...(truncated && { toolOutputPreview: preview }),
1327
1346
  ...(truncated && { originalSize }),
1347
+ ...(artifactId && { artifactId }),
1328
1348
  };
1329
1349
  // Build result — success/error metadata only, NOT the output data
1330
1350
  const result = {
@@ -27,6 +27,12 @@ export declare class ExternalServerManager extends EventEmitter {
27
27
  constructor(config?: ExternalMCPManagerConfig, options?: {
28
28
  enableMainRegistryIntegration?: boolean;
29
29
  });
30
+ /**
31
+ * Attach a McpOutputNormalizer to the underlying ToolDiscoveryService.
32
+ * All tool outputs will be measured and (if oversized) replaced with compact
33
+ * surrogates before being returned to callers.
34
+ */
35
+ setOutputNormalizer(normalizer: import("./mcpOutputNormalizer.js").McpOutputNormalizer): void;
30
36
  /**
31
37
  * Set HITL manager for human-in-the-loop safety mechanisms
32
38
  * @param hitlManager - HITL manager instance (optional, can be undefined to disable)
@@ -210,6 +210,15 @@ export class ExternalServerManager extends EventEmitter {
210
210
  process.on("SIGTERM", () => this.shutdown());
211
211
  process.on("beforeExit", () => this.shutdown());
212
212
  }
213
+ /**
214
+ * Attach a McpOutputNormalizer to the underlying ToolDiscoveryService.
215
+ * All tool outputs will be measured and (if oversized) replaced with compact
216
+ * surrogates before being returned to callers.
217
+ */
218
+ setOutputNormalizer(normalizer) {
219
+ this.toolDiscovery.setOutputNormalizer(normalizer);
220
+ mcpLogger.debug("[ExternalServerManager] MCP output normalizer attached to ToolDiscoveryService");
221
+ }
213
222
  /**
214
223
  * Set HITL manager for human-in-the-loop safety mechanisms
215
224
  * @param hitlManager - HITL manager instance (optional, can be undefined to disable)
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MCP Output Normalizer
3
+ *
4
+ * Single responsibility: intercept a raw MCP `CallToolResult`, measure it,
5
+ * and apply the configured strategy so that oversized payloads never reach
6
+ * caches, Redis, or LLM context windows raw.
7
+ *
8
+ * Two strategies:
9
+ * - "inline" Pass through unchanged. The full payload enters the LLM
10
+ * context as-is. Emit a warning above warnBytes.
11
+ * - "externalize" Write the full payload to the ArtifactStore, return a
12
+ * compact surrogate with a head/tail preview and an artifact
13
+ * ID. The model uses `retrieve_context` with that ID to read
14
+ * the full output on demand, with offset/limit pagination.
15
+ *
16
+ * The surrogate result is shaped as an MCP `CallToolResult` so it passes
17
+ * transparently through any downstream code that expects that format.
18
+ * A `_meta` extension carries the artifact ID for structured extraction in
19
+ * `redisConversationMemoryManager`.
20
+ *
21
+ * @module mcp/mcpOutputNormalizer
22
+ */
23
+ import type { ArtifactStore } from "../artifacts/artifactStore.js";
24
+ import type { McpOutputNormalizerConfig, McpOutputContext, NormalizedMcpOutput } from "../types/mcpOutputTypes.js";
25
+ export type { McpOutputStrategy, McpOutputNormalizerConfig, McpOutputContext, NormalizedMcpOutput, } from "../types/mcpOutputTypes.js";
26
+ /** Default byte ceiling above which externalize fires (100 KB). */
27
+ export declare const DEFAULT_MAX_MCP_OUTPUT_BYTES: number;
28
+ /** Default byte threshold for emitting a warning while still inline (50 KB). */
29
+ export declare const DEFAULT_WARN_MCP_OUTPUT_BYTES: number;
30
+ /** Metadata key embedded in surrogate `_meta` and used by memory manager. */
31
+ export declare const NEUROLINK_ARTIFACT_ID_KEY = "neurolinkArtifactId";
32
+ /**
33
+ * Stateless normalizer (state lives in the injected ArtifactStore).
34
+ *
35
+ * Construct once per NeuroLink instance and set via
36
+ * `ToolDiscoveryService.setOutputNormalizer()`.
37
+ */
38
+ export declare class McpOutputNormalizer {
39
+ private readonly config;
40
+ private readonly artifactStore?;
41
+ constructor(config: McpOutputNormalizerConfig, artifactStore?: ArtifactStore | undefined);
42
+ /**
43
+ * Measure `callResult`, apply strategy if oversized, return normalized output.
44
+ *
45
+ * Never throws: on any internal failure the raw result is returned unchanged
46
+ * with a warning log so tool execution is never broken by the normalizer.
47
+ */
48
+ normalize(callResult: unknown, context: McpOutputContext): Promise<NormalizedMcpOutput>;
49
+ }