@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.
- package/CHANGELOG.md +12 -0
- package/dist/artifacts/artifactStore.d.ts +56 -0
- package/dist/artifacts/artifactStore.js +143 -0
- package/dist/browser/neurolink.min.js +314 -303
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +128 -86
- package/dist/cli/factories/commandFactory.js +3 -2
- package/dist/cli/utils/typewriter.d.ts +18 -0
- package/dist/cli/utils/typewriter.js +42 -0
- package/dist/core/modules/ToolsManager.d.ts +10 -0
- package/dist/core/modules/ToolsManager.js +108 -32
- package/dist/core/redisConversationMemoryManager.js +20 -0
- package/dist/lib/artifacts/artifactStore.d.ts +56 -0
- package/dist/lib/artifacts/artifactStore.js +144 -0
- package/dist/lib/core/modules/ToolsManager.d.ts +10 -0
- package/dist/lib/core/modules/ToolsManager.js +108 -32
- package/dist/lib/core/redisConversationMemoryManager.js +20 -0
- package/dist/lib/mcp/externalServerManager.d.ts +6 -0
- package/dist/lib/mcp/externalServerManager.js +9 -0
- package/dist/lib/mcp/mcpOutputNormalizer.d.ts +49 -0
- package/dist/lib/mcp/mcpOutputNormalizer.js +182 -0
- package/dist/lib/mcp/toolDiscoveryService.d.ts +10 -0
- package/dist/lib/mcp/toolDiscoveryService.js +32 -1
- package/dist/lib/memory/memoryRetrievalTools.d.ts +64 -9
- package/dist/lib/memory/memoryRetrievalTools.js +77 -9
- package/dist/lib/neurolink.d.ts +2 -0
- package/dist/lib/neurolink.js +97 -88
- package/dist/lib/session/globalSessionState.js +44 -1
- package/dist/lib/types/artifactTypes.d.ts +63 -0
- package/dist/lib/types/artifactTypes.js +11 -0
- package/dist/lib/types/configTypes.d.ts +34 -2
- package/dist/lib/types/conversation.d.ts +7 -0
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/mcpOutputTypes.d.ts +40 -0
- package/dist/lib/types/mcpOutputTypes.js +9 -0
- package/dist/mcp/externalServerManager.d.ts +6 -0
- package/dist/mcp/externalServerManager.js +9 -0
- package/dist/mcp/mcpOutputNormalizer.d.ts +49 -0
- package/dist/mcp/mcpOutputNormalizer.js +181 -0
- package/dist/mcp/toolDiscoveryService.d.ts +10 -0
- package/dist/mcp/toolDiscoveryService.js +32 -1
- package/dist/memory/memoryRetrievalTools.d.ts +64 -9
- package/dist/memory/memoryRetrievalTools.js +77 -9
- package/dist/neurolink.d.ts +2 -0
- package/dist/neurolink.js +97 -88
- package/dist/session/globalSessionState.js +44 -1
- package/dist/types/artifactTypes.d.ts +63 -0
- package/dist/types/artifactTypes.js +10 -0
- package/dist/types/configTypes.d.ts +34 -2
- package/dist/types/conversation.d.ts +7 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/mcpOutputTypes.d.ts +40 -0
- package/dist/types/mcpOutputTypes.js +8 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
8
8
|
import type { ExternalMCPToolInfo, ExternalMCPToolResult } from "../types/externalMcp.js";
|
|
9
9
|
import type { ToolDiscoveryResult, ExternalToolExecutionOptions } from "../types/mcpTypes.js";
|
|
10
10
|
import type { JsonObject } from "../types/common.js";
|
|
11
|
+
import type { McpOutputNormalizer } from "./mcpOutputNormalizer.js";
|
|
11
12
|
/**
|
|
12
13
|
* ToolDiscoveryService
|
|
13
14
|
* Handles automatic tool discovery and registration from external MCP servers
|
|
@@ -17,7 +18,16 @@ export declare class ToolDiscoveryService extends EventEmitter {
|
|
|
17
18
|
private toolRegistry;
|
|
18
19
|
private serverTools;
|
|
19
20
|
private discoveryInProgress;
|
|
21
|
+
/** Optional normalizer applied to every tool output before it is returned. */
|
|
22
|
+
private outputNormalizer?;
|
|
20
23
|
constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Attach a McpOutputNormalizer.
|
|
26
|
+
* When set, every raw callTool() result is passed through the normalizer
|
|
27
|
+
* before being returned. Oversized outputs are replaced with compact
|
|
28
|
+
* surrogates according to the configured strategy.
|
|
29
|
+
*/
|
|
30
|
+
setOutputNormalizer(normalizer: McpOutputNormalizer): void;
|
|
21
31
|
/**
|
|
22
32
|
* Discover tools from an external MCP server
|
|
23
33
|
*/
|
|
@@ -26,9 +26,20 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
26
26
|
toolRegistry = new Map();
|
|
27
27
|
serverTools = new Map();
|
|
28
28
|
discoveryInProgress = new Set();
|
|
29
|
+
/** Optional normalizer applied to every tool output before it is returned. */
|
|
30
|
+
outputNormalizer;
|
|
29
31
|
constructor() {
|
|
30
32
|
super();
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Attach a McpOutputNormalizer.
|
|
36
|
+
* When set, every raw callTool() result is passed through the normalizer
|
|
37
|
+
* before being returned. Oversized outputs are replaced with compact
|
|
38
|
+
* surrogates according to the configured strategy.
|
|
39
|
+
*/
|
|
40
|
+
setOutputNormalizer(normalizer) {
|
|
41
|
+
this.outputNormalizer = normalizer;
|
|
42
|
+
}
|
|
32
43
|
/**
|
|
33
44
|
* Discover tools from an external MCP server
|
|
34
45
|
*/
|
|
@@ -361,6 +372,26 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
361
372
|
arguments: parameters,
|
|
362
373
|
}), timeout, new Error(`Tool execution timeout: ${toolName}`));
|
|
363
374
|
callSpan.setStatus({ code: SpanStatusCode.OK });
|
|
375
|
+
// ── MCP output normalization ──────────────────────────────────
|
|
376
|
+
// Intercept here — after receive, before cache, before memory,
|
|
377
|
+
// before LLM context injection. Returns a compact surrogate when
|
|
378
|
+
// the payload exceeds mcp.outputLimits.maxBytes.
|
|
379
|
+
if (this.outputNormalizer) {
|
|
380
|
+
try {
|
|
381
|
+
const normalized = await this.outputNormalizer.normalize(callResult, { toolName, serverId });
|
|
382
|
+
callSpan.setAttribute("mcp.output.strategy", normalized.isExternalized ? "externalize" : "inline");
|
|
383
|
+
if (normalized.isExternalized) {
|
|
384
|
+
callSpan.setAttribute("mcp.output.original_bytes", normalized.originalBytes);
|
|
385
|
+
}
|
|
386
|
+
return normalized.result;
|
|
387
|
+
}
|
|
388
|
+
catch (normErr) {
|
|
389
|
+
mcpLogger.warn(`[ToolDiscoveryService] McpOutputNormalizer failed for ` +
|
|
390
|
+
`${toolName}: ${normErr instanceof Error ? normErr.message : String(normErr)} ` +
|
|
391
|
+
`— returning raw result`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// ── end normalization ─────────────────────────────────────────
|
|
364
395
|
return callResult;
|
|
365
396
|
}
|
|
366
397
|
catch (err) {
|
|
@@ -385,7 +416,7 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
385
416
|
}
|
|
386
417
|
mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
|
|
387
418
|
duration,
|
|
388
|
-
hasContent: !!result
|
|
419
|
+
hasContent: !!result?.content,
|
|
389
420
|
});
|
|
390
421
|
return {
|
|
391
422
|
success: true,
|
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import type { RedisConversationMemoryManager } from "../core/redisConversationMemoryManager.js";
|
|
10
|
+
import type { ArtifactStore } from "../artifacts/artifactStore.js";
|
|
10
11
|
/**
|
|
11
12
|
* Factory function that creates memory retrieval tools bound to a memory manager.
|
|
12
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* @param memoryManager Redis conversation memory manager instance.
|
|
15
|
+
* @param artifactStore Optional artifact store for externalized MCP outputs.
|
|
16
|
+
* When provided, retrieve_context gains an `artifactId`
|
|
17
|
+
* parameter that fetches the full payload written by
|
|
18
|
+
* McpOutputNormalizer under strategy="externalize".
|
|
13
19
|
* @returns Record of tool name to Vercel AI SDK tool definition
|
|
14
20
|
*/
|
|
15
|
-
export declare function createMemoryRetrievalTools(memoryManager: RedisConversationMemoryManager): {
|
|
21
|
+
export declare function createMemoryRetrievalTools(memoryManager: RedisConversationMemoryManager | undefined, artifactStore?: ArtifactStore): {
|
|
16
22
|
retrieve_context: import("ai").Tool<{
|
|
17
|
-
sessionId
|
|
23
|
+
sessionId?: string | undefined;
|
|
24
|
+
artifactId?: string | undefined;
|
|
18
25
|
messageId?: string | undefined;
|
|
19
26
|
role?: "system" | "user" | "assistant" | "tool_call" | "tool_result" | undefined;
|
|
20
27
|
lastN?: number | undefined;
|
|
@@ -22,14 +29,62 @@ export declare function createMemoryRetrievalTools(memoryManager: RedisConversat
|
|
|
22
29
|
limit?: number | undefined;
|
|
23
30
|
search?: string | undefined;
|
|
24
31
|
}, {
|
|
32
|
+
error: string;
|
|
33
|
+
artifactId: string;
|
|
34
|
+
content?: undefined;
|
|
35
|
+
totalSize?: undefined;
|
|
36
|
+
hasMore?: undefined;
|
|
37
|
+
offset?: undefined;
|
|
38
|
+
limit?: undefined;
|
|
39
|
+
sessionId?: undefined;
|
|
40
|
+
messageId?: undefined;
|
|
41
|
+
messages?: undefined;
|
|
42
|
+
totalMessages?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
artifactId: string;
|
|
45
|
+
content: string;
|
|
46
|
+
totalSize: number;
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
offset: number;
|
|
49
|
+
limit: number;
|
|
50
|
+
error?: undefined;
|
|
51
|
+
sessionId?: undefined;
|
|
52
|
+
messageId?: undefined;
|
|
53
|
+
messages?: undefined;
|
|
54
|
+
totalMessages?: undefined;
|
|
55
|
+
} | {
|
|
56
|
+
error: string;
|
|
57
|
+
artifactId?: undefined;
|
|
58
|
+
content?: undefined;
|
|
59
|
+
totalSize?: undefined;
|
|
60
|
+
hasMore?: undefined;
|
|
61
|
+
offset?: undefined;
|
|
62
|
+
limit?: undefined;
|
|
63
|
+
sessionId?: undefined;
|
|
64
|
+
messageId?: undefined;
|
|
65
|
+
messages?: undefined;
|
|
66
|
+
totalMessages?: undefined;
|
|
67
|
+
} | {
|
|
25
68
|
error: string;
|
|
26
69
|
sessionId: string;
|
|
70
|
+
artifactId?: undefined;
|
|
71
|
+
content?: undefined;
|
|
72
|
+
totalSize?: undefined;
|
|
73
|
+
hasMore?: undefined;
|
|
74
|
+
offset?: undefined;
|
|
75
|
+
limit?: undefined;
|
|
27
76
|
messageId?: undefined;
|
|
28
77
|
messages?: undefined;
|
|
29
78
|
totalMessages?: undefined;
|
|
30
79
|
} | {
|
|
31
80
|
error: string;
|
|
32
81
|
messageId: string;
|
|
82
|
+
artifactId?: undefined;
|
|
83
|
+
content?: undefined;
|
|
84
|
+
totalSize?: undefined;
|
|
85
|
+
hasMore?: undefined;
|
|
86
|
+
offset?: undefined;
|
|
87
|
+
limit?: undefined;
|
|
33
88
|
sessionId?: undefined;
|
|
34
89
|
messages?: undefined;
|
|
35
90
|
totalMessages?: undefined;
|
|
@@ -70,13 +125,13 @@ export declare function createMemoryRetrievalTools(memoryManager: RedisConversat
|
|
|
70
125
|
})[];
|
|
71
126
|
totalMessages: number;
|
|
72
127
|
error?: undefined;
|
|
128
|
+
artifactId?: undefined;
|
|
129
|
+
content?: undefined;
|
|
130
|
+
totalSize?: undefined;
|
|
131
|
+
hasMore?: undefined;
|
|
132
|
+
offset?: undefined;
|
|
133
|
+
limit?: undefined;
|
|
73
134
|
sessionId?: undefined;
|
|
74
135
|
messageId?: undefined;
|
|
75
|
-
} | {
|
|
76
|
-
error: string;
|
|
77
|
-
sessionId?: undefined;
|
|
78
|
-
messageId?: undefined;
|
|
79
|
-
messages?: undefined;
|
|
80
|
-
totalMessages?: undefined;
|
|
81
136
|
}>;
|
|
82
137
|
};
|
|
@@ -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
|
-
*
|
|
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
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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
|
|
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(
|
|
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
|
|
147
|
+
return { error: "Session not found", sessionId };
|
|
80
148
|
}
|
|
81
149
|
let messages = conversation.messages;
|
|
82
150
|
// Filter by specific messageId
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -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/lib/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 —
|
|
779
|
-
|
|
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
|
|
782
|
-
maxSize: mcpConfig
|
|
783
|
-
strategy: mcpConfig
|
|
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
|
|
787
|
-
maxSize: mcpConfig
|
|
788
|
-
strategy: mcpConfig
|
|
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
|
-
|
|
940
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
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;
|