@juspay/neurolink 9.51.2 → 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 (54) hide show
  1. package/CHANGELOG.md +12 -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 +314 -303
  5. package/dist/cli/commands/mcp.d.ts +6 -0
  6. package/dist/cli/commands/mcp.js +128 -86
  7. package/dist/cli/factories/commandFactory.js +3 -2
  8. package/dist/cli/utils/typewriter.d.ts +18 -0
  9. package/dist/cli/utils/typewriter.js +42 -0
  10. package/dist/core/modules/ToolsManager.d.ts +10 -0
  11. package/dist/core/modules/ToolsManager.js +108 -32
  12. package/dist/core/redisConversationMemoryManager.js +20 -0
  13. package/dist/lib/artifacts/artifactStore.d.ts +56 -0
  14. package/dist/lib/artifacts/artifactStore.js +144 -0
  15. package/dist/lib/core/modules/ToolsManager.d.ts +10 -0
  16. package/dist/lib/core/modules/ToolsManager.js +108 -32
  17. package/dist/lib/core/redisConversationMemoryManager.js +20 -0
  18. package/dist/lib/mcp/externalServerManager.d.ts +6 -0
  19. package/dist/lib/mcp/externalServerManager.js +9 -0
  20. package/dist/lib/mcp/mcpOutputNormalizer.d.ts +49 -0
  21. package/dist/lib/mcp/mcpOutputNormalizer.js +182 -0
  22. package/dist/lib/mcp/toolDiscoveryService.d.ts +10 -0
  23. package/dist/lib/mcp/toolDiscoveryService.js +32 -1
  24. package/dist/lib/memory/memoryRetrievalTools.d.ts +64 -9
  25. package/dist/lib/memory/memoryRetrievalTools.js +77 -9
  26. package/dist/lib/neurolink.d.ts +2 -0
  27. package/dist/lib/neurolink.js +97 -88
  28. package/dist/lib/session/globalSessionState.js +44 -1
  29. package/dist/lib/types/artifactTypes.d.ts +63 -0
  30. package/dist/lib/types/artifactTypes.js +11 -0
  31. package/dist/lib/types/configTypes.d.ts +34 -2
  32. package/dist/lib/types/conversation.d.ts +7 -0
  33. package/dist/lib/types/index.d.ts +2 -0
  34. package/dist/lib/types/mcpOutputTypes.d.ts +40 -0
  35. package/dist/lib/types/mcpOutputTypes.js +9 -0
  36. package/dist/mcp/externalServerManager.d.ts +6 -0
  37. package/dist/mcp/externalServerManager.js +9 -0
  38. package/dist/mcp/mcpOutputNormalizer.d.ts +49 -0
  39. package/dist/mcp/mcpOutputNormalizer.js +181 -0
  40. package/dist/mcp/toolDiscoveryService.d.ts +10 -0
  41. package/dist/mcp/toolDiscoveryService.js +32 -1
  42. package/dist/memory/memoryRetrievalTools.d.ts +64 -9
  43. package/dist/memory/memoryRetrievalTools.js +77 -9
  44. package/dist/neurolink.d.ts +2 -0
  45. package/dist/neurolink.js +97 -88
  46. package/dist/session/globalSessionState.js +44 -1
  47. package/dist/types/artifactTypes.d.ts +63 -0
  48. package/dist/types/artifactTypes.js +10 -0
  49. package/dist/types/configTypes.d.ts +34 -2
  50. package/dist/types/conversation.d.ts +7 -0
  51. package/dist/types/index.d.ts +2 -0
  52. package/dist/types/mcpOutputTypes.d.ts +40 -0
  53. package/dist/types/mcpOutputTypes.js +8 -0
  54. package/package.json +1 -1
@@ -9,6 +9,7 @@
9
9
  import { tool } from "ai";
10
10
  import { z } from "zod";
11
11
  import { logger } from "../utils/logger.js";
12
+ import { withTimeout } from "../utils/errorHandling.js";
12
13
  import { SpanSerializer, SpanType, SpanStatus, } from "../observability/index.js";
13
14
  import { getMetricsAggregator } from "../observability/index.js";
14
15
  /** Maximum characters returned per retrieval request */
@@ -19,18 +20,36 @@ const MAX_RETRIEVAL_LIMIT = 200_000;
19
20
  const MAX_SEARCH_MATCHES = 50;
20
21
  /**
21
22
  * Factory function that creates memory retrieval tools bound to a memory manager.
22
- * @param memoryManager - The Redis conversation memory manager instance
23
+ *
24
+ * @param memoryManager Redis conversation memory manager instance.
25
+ * @param artifactStore Optional artifact store for externalized MCP outputs.
26
+ * When provided, retrieve_context gains an `artifactId`
27
+ * parameter that fetches the full payload written by
28
+ * McpOutputNormalizer under strategy="externalize".
23
29
  * @returns Record of tool name to Vercel AI SDK tool definition
24
30
  */
25
- export function createMemoryRetrievalTools(memoryManager) {
31
+ export function createMemoryRetrievalTools(memoryManager, artifactStore) {
26
32
  return {
27
33
  retrieve_context: tool({
28
- description: "Retrieve messages from conversation memory. Use this to access full tool " +
29
- "outputs when a result was truncated, review previous assistant responses, " +
30
- "or search through conversation history. Supports filtering by role, " +
31
- "pagination for large content, and regex search within messages.",
34
+ description: "Retrieve messages from conversation memory, or fetch the full payload of " +
35
+ "an externalized MCP tool output by artifact ID. Use this to:\n" +
36
+ " Access full tool outputs when a result was truncated or externalized\n" +
37
+ " Review previous assistant responses\n" +
38
+ "• Search through conversation history\n" +
39
+ "Supports filtering by role, pagination for large content, and regex search.\n" +
40
+ "To fetch an externalized artifact, provide `artifactId` (omit sessionId).",
32
41
  inputSchema: z.object({
33
- sessionId: z.string().describe("Session ID for the conversation"),
42
+ sessionId: z
43
+ .string()
44
+ .optional()
45
+ .describe("Session ID for conversation history retrieval. " +
46
+ "Required unless artifactId is provided."),
47
+ artifactId: z
48
+ .string()
49
+ .optional()
50
+ .describe("Artifact ID from an externalized MCP tool output " +
51
+ "(visible in the tool output as neurolinkArtifactId=<id>). " +
52
+ "When provided, returns the full stored payload directly."),
34
53
  messageId: z
35
54
  .string()
36
55
  .optional()
@@ -64,19 +83,68 @@ export function createMemoryRetrievalTools(memoryManager) {
64
83
  "Returns matching lines with line numbers."),
65
84
  }),
66
85
  execute: async (args) => {
86
+ // ── Artifact resolution path ────────────────────────────────────────
87
+ // When the caller supplies an artifactId we short-circuit to the
88
+ // artifact store (bypassing Redis) and return the full payload with
89
+ // optional offset/limit pagination.
90
+ if (args.artifactId) {
91
+ if (!artifactStore) {
92
+ logger.warn("[MemoryRetrievalTools] retrieve_context called with artifactId " +
93
+ "but no ArtifactStore is configured");
94
+ return {
95
+ error: "Artifact store not configured — " +
96
+ "mcp.outputLimits.strategy must be set to 'externalize' to use artifactId retrieval",
97
+ artifactId: args.artifactId,
98
+ };
99
+ }
100
+ const content = await withTimeout(artifactStore.retrieve(args.artifactId), 10_000, new Error(`ArtifactStore.retrieve() timed out for artifact "${args.artifactId}"`));
101
+ if (content === null) {
102
+ return {
103
+ error: "Artifact not found or has expired",
104
+ artifactId: args.artifactId,
105
+ };
106
+ }
107
+ const charLimit = Math.min(args.limit ?? DEFAULT_RETRIEVAL_LIMIT, MAX_RETRIEVAL_LIMIT);
108
+ const start = args.offset ?? 0;
109
+ const slice = content.slice(start, start + charLimit);
110
+ return {
111
+ artifactId: args.artifactId,
112
+ content: slice,
113
+ totalSize: content.length,
114
+ hasMore: start + charLimit < content.length,
115
+ offset: start,
116
+ limit: charLimit,
117
+ };
118
+ }
119
+ // ── End artifact resolution ─────────────────────────────────────────
120
+ if (!args.sessionId) {
121
+ return {
122
+ error: "sessionId is required when artifactId is not provided",
123
+ };
124
+ }
125
+ if (!memoryManager) {
126
+ return {
127
+ error: "Session history retrieval requires Redis conversation memory — " +
128
+ "enable mcp.conversationMemory with a Redis backend, or use " +
129
+ "artifactId to retrieve an externalized MCP tool output.",
130
+ };
131
+ }
67
132
  const span = SpanSerializer.createSpan(SpanType.MEMORY, "memory.retrieve", {
68
133
  "memory.operation": "retrieve",
69
134
  "memory.store": "redis",
70
135
  "memory.query": args.search || args.messageId || `lastN:${args.lastN ?? "all"}`,
71
136
  });
72
137
  const startTime = Date.now();
138
+ // args.sessionId is guaranteed non-null here — we returned early above
139
+ // when it was missing. Cast via string coercion to satisfy eslint.
140
+ const sessionId = String(args.sessionId);
73
141
  try {
74
- const conversation = await memoryManager.getSessionRaw(args.sessionId);
142
+ const conversation = await withTimeout(memoryManager.getSessionRaw(sessionId), 10_000, new Error(`getSessionRaw() timed out for session "${sessionId}"`));
75
143
  if (!conversation) {
76
144
  span.durationMs = Date.now() - startTime;
77
145
  const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK);
78
146
  getMetricsAggregator().recordSpan(endedSpan);
79
- return { error: "Session not found", sessionId: args.sessionId };
147
+ return { error: "Session not found", sessionId };
80
148
  }
81
149
  let messages = conversation.messages;
82
150
  // Filter by specific messageId
@@ -47,6 +47,8 @@ export declare class NeuroLink {
47
47
  private mcpToolBatcher?;
48
48
  private mcpEnhancedDiscovery?;
49
49
  private mcpToolMiddlewares;
50
+ /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
51
+ private mcpArtifactStore?;
50
52
  private _disableToolCacheForCurrentRequest;
51
53
  private mcpEnhancementsConfig?;
52
54
  private toolCircuitBreakers;
package/dist/neurolink.js CHANGED
@@ -40,6 +40,8 @@ import { ToolCallBatcher } from "./mcp/batching/index.js";
40
40
  import { ToolResultCache } from "./mcp/caching/index.js";
41
41
  import { EnhancedToolDiscovery } from "./mcp/enhancedToolDiscovery.js";
42
42
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
43
+ import { McpOutputNormalizer, DEFAULT_MAX_MCP_OUTPUT_BYTES, DEFAULT_WARN_MCP_OUTPUT_BYTES, } from "./mcp/mcpOutputNormalizer.js";
44
+ import { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
43
45
  import { ToolRouter } from "./mcp/routing/index.js";
44
46
  // Import direct tools server for automatic registration
45
47
  import { directToolsServer } from "./mcp/servers/agent/directToolsServer.js";
@@ -216,6 +218,8 @@ export class NeuroLink {
216
218
  mcpToolBatcher;
217
219
  mcpEnhancedDiscovery;
218
220
  mcpToolMiddlewares = [];
221
+ /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
222
+ mcpArtifactStore;
219
223
  _disableToolCacheForCurrentRequest = false;
220
224
  mcpEnhancementsConfig;
221
225
  // Enhanced error handling support
@@ -775,17 +779,18 @@ export class NeuroLink {
775
779
  initializeMCPEnhancements(config) {
776
780
  const mcpConfig = config?.mcp;
777
781
  this.mcpEnhancementsConfig = mcpConfig;
778
- // ToolCache — disabled by default, opt-in
779
- if (mcpConfig?.cache?.enabled) {
782
+ // BZ-664: ToolCache — enabled by default to prevent duplicate tool calls.
783
+ // Callers can explicitly opt out via mcp.cache.enabled = false.
784
+ if (mcpConfig?.cache?.enabled !== false) {
780
785
  this.mcpToolResultCache = new ToolResultCache({
781
- ttl: mcpConfig.cache.ttl ?? 300_000,
782
- maxSize: mcpConfig.cache.maxSize ?? 500,
783
- strategy: mcpConfig.cache.strategy ?? "lru",
786
+ ttl: mcpConfig?.cache?.ttl ?? 300_000,
787
+ maxSize: mcpConfig?.cache?.maxSize ?? 500,
788
+ strategy: mcpConfig?.cache?.strategy ?? "lru",
784
789
  });
785
790
  logger.debug("[NeuroLink] MCP tool result cache initialized", {
786
- ttl: mcpConfig.cache.ttl ?? 300_000,
787
- maxSize: mcpConfig.cache.maxSize ?? 500,
788
- strategy: mcpConfig.cache.strategy ?? "lru",
791
+ ttl: mcpConfig?.cache?.ttl ?? 300_000,
792
+ maxSize: mcpConfig?.cache?.maxSize ?? 500,
793
+ strategy: mcpConfig?.cache?.strategy ?? "lru",
789
794
  });
790
795
  }
791
796
  // ToolCallBatcher — disabled by default, opt-in
@@ -817,6 +822,25 @@ export class NeuroLink {
817
822
  });
818
823
  }
819
824
  // ToolRouter — lazy-initialized when 2+ external servers exist (see addExternalMCPServer)
825
+ // McpOutputNormalizer — active when mcp.outputLimits is configured
826
+ if (mcpConfig?.outputLimits) {
827
+ const strategy = mcpConfig.outputLimits.strategy ?? "externalize";
828
+ const maxBytes = mcpConfig.outputLimits.maxBytes ?? DEFAULT_MAX_MCP_OUTPUT_BYTES;
829
+ const warnBytes = mcpConfig.outputLimits.warnBytes ?? DEFAULT_WARN_MCP_OUTPUT_BYTES;
830
+ let artifactStore;
831
+ if (strategy === "externalize") {
832
+ artifactStore = new LocalTempArtifactStore();
833
+ this.mcpArtifactStore = artifactStore;
834
+ logger.debug("[NeuroLink] MCP artifact store initialized (local-temp)");
835
+ }
836
+ const normalizer = new McpOutputNormalizer({ strategy, maxBytes, warnBytes }, artifactStore);
837
+ this.externalServerManager.setOutputNormalizer(normalizer);
838
+ logger.debug("[NeuroLink] MCP output normalizer initialized", {
839
+ strategy,
840
+ maxBytes,
841
+ warnBytes,
842
+ });
843
+ }
820
844
  }
821
845
  /**
822
846
  * Register file reference tools with the MCP tool registry.
@@ -936,90 +960,46 @@ export class NeuroLink {
936
960
  "redis" in memConfig &&
937
961
  !!memConfig.redis) ||
938
962
  process.env.STORAGE_TYPE === "redis";
939
- if (!memConfig?.enabled || !hasRedisConfig) {
940
- logger.debug("[NeuroLink] Skipping memory retrieval tools requires Redis conversation memory");
963
+ const hasArtifactStore = !!this.mcpArtifactStore;
964
+ // Register when Redis is configured OR when an artifact store exists.
965
+ // Artifact store alone is sufficient for the artifactId retrieval path —
966
+ // session history retrieval just returns a clear error when Redis is absent.
967
+ if ((!memConfig?.enabled || !hasRedisConfig) && !hasArtifactStore) {
968
+ logger.debug("[NeuroLink] Skipping memory retrieval tools — requires Redis conversation memory or an artifact store");
941
969
  return;
942
970
  }
943
- const tools = {
944
- retrieve_context: {
945
- description: "Retrieve messages from conversation memory. Use this to access full tool " +
946
- "outputs when a result was truncated, review previous assistant responses, " +
947
- "or search through conversation history.",
948
- execute: async (params) => {
949
- // Lazy access: conversationMemory is initialized on first generate() call
950
- const memoryManager = this.conversationMemory;
951
- if (!memoryManager || !("getSessionRaw" in memoryManager)) {
952
- return {
953
- success: false,
954
- error: "Memory retrieval not available — Redis memory manager not initialized",
955
- metadata: {
956
- toolName: "retrieve_context",
957
- serverId: "direct",
958
- executionTime: 0,
959
- },
960
- };
961
- }
962
- const actualTools = createMemoryRetrievalTools(memoryManager);
963
- const result = await actualTools.retrieve_context.execute(params, {
964
- toolCallId: "memory-retrieval",
965
- messages: [],
966
- });
967
- // Check if the tool itself reported an error
968
- const hasError = result &&
969
- typeof result === "object" &&
970
- "error" in result &&
971
- !("messages" in result);
972
- const errorMsg = hasError
973
- ? result.error
974
- : undefined;
975
- return {
976
- success: !hasError,
977
- data: result,
978
- ...(errorMsg ? { error: errorMsg } : {}),
979
- metadata: {
980
- toolName: "retrieve_context",
981
- serverId: "direct",
982
- executionTime: 0,
983
- },
984
- };
985
- },
971
+ // Extract the canonical tool definition (schema + description) from the
972
+ // memoryRetrievalTools factory. We pass undefined as the memoryManager here
973
+ // because we only need the Zod inputSchema and description at registration
974
+ // time the actual manager is resolved lazily at execution time.
975
+ const canonicalTools = createMemoryRetrievalTools(undefined, this.mcpArtifactStore);
976
+ const retrieveContextDef = canonicalTools.retrieve_context;
977
+ // Register via this.registerTool() so the tool ends up in the "user-defined"
978
+ // category inside toolRegistry. getCustomTools() returns that category, which
979
+ // is what ToolsManager reads to build the tool schema sent to the LLM.
980
+ // (Tools registered via toolRegistry.registerTool() directly land in the
981
+ // "built-in" category and are never included in the LLM's tool schema.)
982
+ this.registerTool("retrieve_context", {
983
+ name: "retrieve_context",
984
+ description: retrieveContextDef.description ?? "Retrieve context or artifacts",
985
+ // Pass the Zod schema so ToolsManager gives the LLM full parameter types.
986
+ // registerTool() detects isZodSchema on inputSchema and preserves it.
987
+ inputSchema: retrieveContextDef
988
+ .inputSchema,
989
+ execute: async (params) => {
990
+ // Lazy: conversationMemory is initialized on the first generate() call.
991
+ // When only an artifact store is present (no Redis), memoryManager is
992
+ // undefined — createMemoryRetrievalTools handles that via an explicit guard.
993
+ const memoryManager = this.conversationMemory;
994
+ const tools = createMemoryRetrievalTools(memoryManager, this.mcpArtifactStore);
995
+ // Return the result directly so the LLM receives clean output instead
996
+ // of a nested { success, data, metadata } wrapper.
997
+ // Bounded by TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS so a stalled Redis or
998
+ // filesystem backend never hangs the tool call indefinitely.
999
+ return await withTimeout(tools.retrieve_context.execute(params, { toolCallId: "memory-retrieval", messages: [] }), TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS, ErrorFactory.toolTimeout("retrieve_context", TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS));
986
1000
  },
987
- };
988
- const registrations = Object.entries(tools).map(async ([toolName, toolDef]) => {
989
- const toolId = `direct.${toolName}`;
990
- const toolInfo = {
991
- name: toolName,
992
- description: toolDef.description,
993
- inputSchema: {},
994
- serverId: "direct",
995
- category: "built-in",
996
- };
997
- await this.toolRegistry.registerTool(toolId, toolInfo, {
998
- execute: async (params) => {
999
- try {
1000
- return await toolDef.execute(params);
1001
- }
1002
- catch (error) {
1003
- // Known limitation: this non-throwing error path returns
1004
- // { success: false } without recording errorCategories in
1005
- // toolExecutionMetrics. These are internal memory-tool failures
1006
- // (low frequency), so the risk of metric gaps is minimal.
1007
- // A full fix would require access to the metrics map here,
1008
- // which is not available in the registration closure.
1009
- return {
1010
- success: false,
1011
- error: error instanceof Error ? error.message : String(error),
1012
- metadata: { toolName, serverId: "direct", executionTime: 0 },
1013
- };
1014
- }
1015
- },
1016
- description: toolDef.description,
1017
- inputSchema: {},
1018
- });
1019
- });
1020
- void Promise.all(registrations).then(() => {
1021
- logger.info("[NeuroLink] Memory retrieval tools registered");
1022
1001
  });
1002
+ logger.info("[NeuroLink] Memory retrieval tools registered");
1023
1003
  }
1024
1004
  /** Format memory context for prompt inclusion */
1025
1005
  formatMemoryContext(memoryContext, currentInput) {
@@ -7628,7 +7608,36 @@ Current user's request: ${currentInput}`;
7628
7608
  async executeExternalMCPTool(serverId, toolName, parameters, options) {
7629
7609
  try {
7630
7610
  mcpLogger.debug(`[NeuroLink] Executing external MCP tool: ${toolName} on ${serverId}`);
7611
+ // BZ-664: Check existing ToolResultCache before executing to avoid
7612
+ // duplicate identical calls within the same session.
7613
+ //
7614
+ // Safety guards aligned with executeToolInternal():
7615
+ // - Skip destructive tools (destructiveHint annotation)
7616
+ // - Scope cache key by serverId (two servers can expose same tool name)
7617
+ // and toolExecutionContext (prevents cross-session/user leaks)
7618
+ const toolAnnotations = this.getToolAnnotationsForExecution(toolName);
7619
+ const cacheEnabled = !!this.mcpToolResultCache &&
7620
+ !this._disableToolCacheForCurrentRequest &&
7621
+ !toolAnnotations?.destructiveHint;
7622
+ const cacheKeyArgs = {
7623
+ __serverId: serverId,
7624
+ __args: parameters,
7625
+ ...(this.toolExecutionContext
7626
+ ? { __ctx: this.toolExecutionContext }
7627
+ : {}),
7628
+ };
7629
+ if (cacheEnabled && this.mcpToolResultCache) {
7630
+ const cached = this.mcpToolResultCache.getCachedResult(toolName, cacheKeyArgs);
7631
+ if (cached !== undefined) {
7632
+ mcpLogger.debug(`[NeuroLink] Tool result cache HIT: ${toolName} on ${serverId}`);
7633
+ return cached;
7634
+ }
7635
+ }
7631
7636
  const result = await this.externalServerManager.executeTool(serverId, toolName, parameters, options);
7637
+ // BZ-664: Store result in cache after successful execution
7638
+ if (cacheEnabled && this.mcpToolResultCache) {
7639
+ this.mcpToolResultCache.cacheResult(toolName, cacheKeyArgs, result);
7640
+ }
7632
7641
  mcpLogger.debug(`[NeuroLink] External MCP tool executed successfully: ${toolName}`);
7633
7642
  return result;
7634
7643
  }
@@ -1,6 +1,33 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { NeuroLink } from "../neurolink.js";
3
3
  import { buildObservabilityConfigFromEnv } from "../utils/observabilityHelpers.js";
4
+ /**
5
+ * Build mcp.outputLimits config from environment variables.
6
+ * Reads NEUROLINK_MCP_OUTPUT_STRATEGY and NEUROLINK_MCP_MAX_OUTPUT_BYTES.
7
+ * Returns undefined when neither variable is set (no overhead).
8
+ */
9
+ function buildMcpOutputLimitsFromEnv() {
10
+ const strategyRaw = process.env.NEUROLINK_MCP_OUTPUT_STRATEGY;
11
+ const maxBytesRaw = process.env.NEUROLINK_MCP_MAX_OUTPUT_BYTES;
12
+ const warnBytesRaw = process.env.NEUROLINK_MCP_WARN_OUTPUT_BYTES;
13
+ if (!strategyRaw && !maxBytesRaw) {
14
+ return undefined;
15
+ }
16
+ const strategy = strategyRaw === "inline" || strategyRaw === "externalize"
17
+ ? strategyRaw
18
+ : "externalize"; // safe default when only maxBytes is set
19
+ const maxBytes = maxBytesRaw ? parseInt(maxBytesRaw, 10) : undefined;
20
+ const warnBytes = warnBytesRaw ? parseInt(warnBytesRaw, 10) : undefined;
21
+ return {
22
+ strategy,
23
+ ...(maxBytes !== undefined && Number.isFinite(maxBytes) && maxBytes >= 0
24
+ ? { maxBytes }
25
+ : {}),
26
+ ...(warnBytes !== undefined && Number.isFinite(warnBytes) && warnBytes >= 0
27
+ ? { warnBytes }
28
+ : {}),
29
+ };
30
+ }
4
31
  export class GlobalSessionManager {
5
32
  static instance;
6
33
  loopSession = null;
@@ -25,6 +52,14 @@ export class GlobalSessionManager {
25
52
  if (observabilityConfig) {
26
53
  neurolinkOptions.observability = observabilityConfig;
27
54
  }
55
+ // Add MCP output limits from environment variables (CLI usage)
56
+ const mcpOutputLimits = buildMcpOutputLimitsFromEnv();
57
+ if (mcpOutputLimits) {
58
+ neurolinkOptions.mcp = {
59
+ ...neurolinkOptions.mcp,
60
+ outputLimits: mcpOutputLimits,
61
+ };
62
+ }
28
63
  this.loopSession = {
29
64
  neurolinkInstance: new NeuroLink(neurolinkOptions),
30
65
  sessionId,
@@ -99,7 +134,15 @@ export class GlobalSessionManager {
99
134
  }
100
135
  // Create new NeuroLink with observability config from environment (CLI usage)
101
136
  const observabilityConfig = buildObservabilityConfigFromEnv();
102
- return new NeuroLink(observabilityConfig ? { observability: observabilityConfig } : undefined);
137
+ const mcpOutputLimits = buildMcpOutputLimitsFromEnv();
138
+ const options = {};
139
+ if (observabilityConfig) {
140
+ options.observability = observabilityConfig;
141
+ }
142
+ if (mcpOutputLimits) {
143
+ options.mcp = { outputLimits: mcpOutputLimits };
144
+ }
145
+ return new NeuroLink(Object.keys(options).length ? options : undefined);
103
146
  }
104
147
  getCurrentSessionId() {
105
148
  return this.getLoopSession()?.sessionId;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Artifact Store Types (canonical location)
3
+ *
4
+ * Types for the MCP large-output artifact storage system.
5
+ * When mcp.outputLimits.strategy = "externalize", oversized MCP tool outputs
6
+ * are stored as artifacts and the model receives a compact surrogate instead.
7
+ *
8
+ * @module types/artifactTypes
9
+ */
10
+ /** Metadata recorded alongside a stored artifact. */
11
+ export type ArtifactMeta = {
12
+ /** Tool name that produced the output. */
13
+ toolName: string;
14
+ /** MCP server ID. */
15
+ serverId: string;
16
+ /** Session that triggered the tool call (optional). */
17
+ sessionId?: string;
18
+ /** Serialized byte size of the full payload. */
19
+ sizeBytes: number;
20
+ /** Whether the payload is valid JSON or plain text. */
21
+ contentType: "json" | "text";
22
+ /** Unix epoch ms when the artifact was created. */
23
+ createdAt: number;
24
+ };
25
+ /** Lightweight descriptor returned after a successful ArtifactStore.store(). */
26
+ export type ArtifactRef = {
27
+ /** UUID v4 — stable identifier used in surrogate results and metadata. */
28
+ id: string;
29
+ /** First N characters of the payload (for surrogate headers). */
30
+ preview: string;
31
+ /** Full serialized byte size. */
32
+ sizeBytes: number;
33
+ /** Stored metadata. */
34
+ meta: ArtifactMeta;
35
+ };
36
+ /**
37
+ * Pluggable storage contract for externalized MCP tool outputs.
38
+ *
39
+ * Default backend: LocalTempArtifactStore (filesystem, single-process).
40
+ * Future backends can implement this interface for S3, Redis blobs, etc.
41
+ */
42
+ export interface ArtifactStore {
43
+ /**
44
+ * Persist a payload and return a lightweight reference.
45
+ * @param payload Serialized tool output (JSON string or plain text).
46
+ * @param meta Descriptor without `createdAt` (assigned internally).
47
+ */
48
+ store(payload: string, meta: Omit<ArtifactMeta, "createdAt">): Promise<ArtifactRef>;
49
+ /**
50
+ * Retrieve the full payload by artifact ID.
51
+ * Returns `null` if the artifact is not found or has been cleaned up.
52
+ */
53
+ retrieve(id: string): Promise<string | null>;
54
+ /** Delete a single artifact. No-op if the ID does not exist. */
55
+ delete(id: string): Promise<void>;
56
+ /**
57
+ * Delete all artifacts older than `olderThanMs` milliseconds.
58
+ * Returns the number of artifacts deleted.
59
+ */
60
+ cleanup(olderThanMs: number): Promise<number>;
61
+ /** Generate a short preview string from a serialized payload. */
62
+ generatePreview(payload: string): string;
63
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Artifact Store Types (canonical location)
3
+ *
4
+ * Types for the MCP large-output artifact storage system.
5
+ * When mcp.outputLimits.strategy = "externalize", oversized MCP tool outputs
6
+ * are stored as artifacts and the model receives a compact surrogate instead.
7
+ *
8
+ * @module types/artifactTypes
9
+ */
10
+ export {};
@@ -44,7 +44,7 @@ export type NeurolinkConstructorConfig = {
44
44
  * Configuration for MCP enhancement modules wired into generate()/stream() paths.
45
45
  *
46
46
  * These modules are automatically applied during tool execution when configured:
47
- * - cache: Tool result caching (disabled by default)
47
+ * - cache: Tool result caching (enabled by default, opt out with enabled: false)
48
48
  * - annotations: Auto-infer tool safety metadata (enabled by default)
49
49
  * - router: Multi-server tool routing (auto-activates with 2+ servers)
50
50
  * - batcher: Batch programmatic tool calls (disabled by default)
@@ -52,7 +52,7 @@ export type NeurolinkConstructorConfig = {
52
52
  * - middleware: Global tool execution middleware chain (empty by default)
53
53
  */
54
54
  export type MCPEnhancementsConfig = {
55
- /** Tool result caching. Default: disabled. Enable to cache read-only tool results. */
55
+ /** Tool result caching. Default: enabled. Set enabled: false to opt out. */
56
56
  cache?: {
57
57
  enabled?: boolean;
58
58
  /** Cache TTL in milliseconds. Default: 300000 (5 min) */
@@ -90,6 +90,38 @@ export type MCPEnhancementsConfig = {
90
90
  };
91
91
  /** Global tool middleware applied to every tool execution. Default: empty. */
92
92
  middleware?: ToolMiddleware[];
93
+ /**
94
+ * Large MCP tool output handling.
95
+ *
96
+ * MCP servers can return arbitrarily large payloads. Without limits these
97
+ * are loaded entirely into memory, cached in full, stored whole in Redis, and
98
+ * injected into the LLM context window — all of which silently fail at scale.
99
+ *
100
+ * When configured, NeuroLink intercepts oversized outputs at the tool boundary
101
+ * (before caching and before memory persistence) and applies the chosen
102
+ * strategy so the model receives a compact surrogate instead of a firehose.
103
+ *
104
+ * Two strategies:
105
+ * - "inline" Keep sending the full payload to the model regardless of
106
+ * size. A warning is emitted above warnBytes.
107
+ * - "externalize" Store the full payload on disk as an artifact and return a
108
+ * compact surrogate with a head/tail preview and an artifact
109
+ * ID. The model uses `retrieve_context` with that ID to read
110
+ * the full output on demand, with offset/limit pagination.
111
+ *
112
+ * Defaults (when `outputLimits` is set):
113
+ * strategy = "externalize"
114
+ * maxBytes = 100 KB (100 * 1024)
115
+ * warnBytes = 50 KB (50 * 1024)
116
+ */
117
+ outputLimits?: {
118
+ /** What to do when output exceeds maxBytes. Default: "externalize". */
119
+ strategy?: "inline" | "externalize";
120
+ /** Byte ceiling above which the strategy fires. Default: 102400 (100 KB). */
121
+ maxBytes?: number;
122
+ /** Bytes at which a warning is emitted even when still inline. Default: 51200 (50 KB). */
123
+ warnBytes?: number;
124
+ };
93
125
  };
94
126
  /**
95
127
  * Authentication configuration for NeuroLink SDK
@@ -231,6 +231,13 @@ export type ChatMessageMetadata = {
231
231
  toolOutputPreview?: string;
232
232
  /** Original byte size of the full tool output before any truncation */
233
233
  originalSize?: number;
234
+ /**
235
+ * Artifact store ID for an externalized MCP tool output.
236
+ * Set when `mcp.outputLimits.strategy = "externalize"` and the tool output
237
+ * exceeded `maxBytes`. Use retrieve_context with this ID to fetch the full
238
+ * payload from the local artifact store.
239
+ */
240
+ artifactId?: string;
234
241
  };
235
242
  /**
236
243
  * Chat message format for conversation history
@@ -6,6 +6,8 @@ export * from "./cli.js";
6
6
  export * from "./common.js";
7
7
  export type { AnalyticsConfig, BackupInfo, BackupMetadata, CacheConfig, ConfigUpdateOptions, ConfigValidationResult, FallbackConfig, MCPEnhancementsConfig, NeuroLinkConfig, PerformanceConfig, RetryConfig, ToolConfig, } from "./configTypes.js";
8
8
  export type { ExternalMCPConfigValidation, ExternalMCPManagerConfig, ExternalMCPOperationResult, ExternalMCPServerEvents, ExternalMCPServerHealth, ExternalMCPServerInstance, ExternalMCPServerStatus, ExternalMCPToolContext, ExternalMCPToolInfo, ExternalMCPToolResult, } from "./externalMcp.js";
9
+ export type { ArtifactMeta, ArtifactRef, ArtifactStore, } from "./artifactTypes.js";
10
+ export type { McpOutputContext, McpOutputNormalizerConfig, McpOutputStrategy, NormalizedMcpOutput, } from "./mcpOutputTypes.js";
9
11
  export type { AuthorizationUrlResult, CircuitBreakerConfig, CircuitBreakerEvents, CircuitBreakerState, CircuitBreakerStats, DiscoveredMcp, ExternalToolExecutionOptions, FlexibleValidationResult, HTTPRetryConfig, MCPClientResult, MCPConnectedServer, MCPDiscoveredServer, MCPExecutableTool, MCPOAuthConfig, MCPServerCategory, MCPServerConfig, MCPServerConnectionStatus, MCPServerInfo, MCPServerMetadata, MCPServerRegistryEntry, MCPServerStatus, MCPStatus, MCPToolInfo, MCPToolMetadata, MCPTransportType, McpMetadata, McpRegistry, NeuroLinkExecutionContext, NeuroLinkMCPServer, NeuroLinkMCPTool, OAuthClientInformation, OAuthTokens as McpOAuthTokens, RateLimitConfig, TokenBucketRateLimitConfig, TokenExchangeRequest, TokenStorage, ToolDiscoveryResult, ToolRegistryEvents, ToolValidationResult, } from "./mcpTypes.js";
10
12
  export type { ModelCapability, ModelFilter, ModelPricing, ModelResolutionContext, ModelStats, ModelUseCase, } from "./providers.js";
11
13
  export * from "./providers.js";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * MCP Output Normalizer Types (canonical location)
3
+ *
4
+ * Types for the large MCP response handling system.
5
+ *
6
+ * @module types/mcpOutputTypes
7
+ */
8
+ /**
9
+ * Two honest strategies for oversized MCP tool outputs:
10
+ * - "inline" Full payload always sent to the model (warning logged above warnBytes).
11
+ * - "externalize" Full payload stored as an artifact; model receives a compact
12
+ * surrogate with head/tail preview and an artifact ID it can
13
+ * resolve via retrieve_context with offset/limit pagination.
14
+ */
15
+ export type McpOutputStrategy = "inline" | "externalize";
16
+ /** Configuration for McpOutputNormalizer. */
17
+ export type McpOutputNormalizerConfig = {
18
+ strategy: McpOutputStrategy;
19
+ /** Byte ceiling above which the strategy fires. */
20
+ maxBytes: number;
21
+ /** Bytes at which a warning is emitted while still inline. */
22
+ warnBytes: number;
23
+ };
24
+ /** Contextual info passed alongside the raw MCP callResult. */
25
+ export type McpOutputContext = {
26
+ toolName: string;
27
+ serverId: string;
28
+ sessionId?: string;
29
+ };
30
+ /** Value returned by McpOutputNormalizer.normalize(). */
31
+ export type NormalizedMcpOutput = {
32
+ /** The result to substitute for the raw callResult. May be a surrogate. */
33
+ result: unknown;
34
+ /** Whether the full payload was written to the artifact store. */
35
+ isExternalized: boolean;
36
+ /** Artifact ID when isExternalized === true. */
37
+ artifactId?: string;
38
+ /** Serialized byte size of the original payload. */
39
+ originalBytes: number;
40
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * MCP Output Normalizer Types (canonical location)
3
+ *
4
+ * Types for the large MCP response handling system.
5
+ *
6
+ * @module types/mcpOutputTypes
7
+ */
8
+ export {};