Skip to main content

MCP Enhancements

Since: v9.16.0 | Status: Stable | Availability: SDK

Overview

The MCP Enhancements suite extends NeuroLink's Model Context Protocol integration with production-grade capabilities for managing tool calls at scale. These modules address the operational challenges of running multiple MCP servers in enterprise environments:

  • Tool Router -- Intelligent routing of tool calls across multiple servers with 6 strategies
  • Tool Cache -- High-performance result caching with LRU, FIFO, and LFU eviction
  • Request Batcher -- Automatic batching of tool calls for improved throughput
  • Tool Annotations -- Safety metadata and behavior hints for MCP tools
  • Tool Converter -- Bidirectional conversion between NeuroLink and MCP tool formats
  • Tool Integration -- Middleware chain for confirmation, retry, timeout, and logging
  • Enhanced Tool Discovery -- Advanced search and filtering across multi-server environments
  • Elicitation Protocol -- Interactive user input during tool execution (HITL)
  • Multi-Server Manager -- Load balancing and failover across server groups
  • MCP Server Base -- Abstract base class for building custom MCP servers
  • Agent & Workflow Exposure -- Expose agents and workflows as MCP tools
  • Server Capabilities -- Resource and prompt management per MCP spec
  • MCP Registry Client -- Discover servers from registries and well-known catalogs

Architecture Diagrams: For visual diagrams of the overall architecture, routing flows, caching strategies, batching sequences, elicitation protocol, and multi-server topology, see MCP Enhancement Architecture Diagrams.

Quick Start

A complete, runnable example showing routing, caching, and batching working together:

import {
ToolRouter,
ToolCache,
RequestBatcher,
createAnnotatedTool,
} from "@juspay/neurolink";

async function main() {
// 1. Set up intelligent routing across servers
const router = new ToolRouter({
strategy: "least-loaded",
enableAffinity: true,
categoryMapping: {
database: ["db-server-1", "db-server-2"],
ai: ["ai-server-primary"],
},
});
router.registerServer("db-server-1", ["database"]);
router.registerServer("db-server-2", ["database"]);
router.registerServer("ai-server-primary", ["ai"]);

// 2. Cache tool results to reduce redundant calls
const cache = new ToolCache({
ttl: 60000,
maxSize: 500,
strategy: "lru",
});

// 3. Create a tool with safety annotations
const tool = createAnnotatedTool({
name: "queryUsers",
description: "Query the users table",
inputSchema: {
type: "object",
properties: { limit: { type: "number" } },
},
execute: async (params) => {
return { users: ["Alice", "Bob"] };
},
});
// Annotations inferred: { readOnlyHint: true, idempotentHint: true }

// 4. Route the tool call to the best server
const decision = router.route(
{ name: "queryUsers", category: "database" },
{ sessionId: "user-123" },
);
console.log(`Routed to: ${decision.serverId} (${decision.strategy})`);

// 5. Execute with caching -- only calls the function on cache miss
const result = await cache.getOrSet(
ToolCache.generateKey("queryUsers", { limit: 10 }),
async () => tool.execute!({ limit: 10 }),
);
console.log("Result:", result);

// 6. Set up batching for high-throughput scenarios
const batcher = new RequestBatcher({
maxBatchSize: 10,
maxWaitMs: 50,
groupByServer: true,
});

batcher.setExecutor(async (requests) => {
return requests.map((r) => ({
success: true as const,
result: { id: r.args.id, name: `User ${r.args.id}` },
}));
});

// Batch multiple calls -- they execute together
const [user1, user2] = await Promise.all([
batcher.add("getUser", { id: 1 }, "db-server-1"),
batcher.add("getUser", { id: 2 }, "db-server-1"),
]);
console.log("Batched results:", user1, user2);

// Clean up
cache.destroy();
batcher.destroy();
}

main().catch(console.error);

Tool Router

Intelligent routing of tool calls to appropriate MCP servers based on categories, annotations, and server capabilities.

Routing Strategies

StrategyDescriptionConfidence
round-robinDistribute calls evenly across servers0.8
least-loadedRoute to server with fewest active connections0.9
capability-basedScore servers by capability match and weightVariable
affinityMaintain session/user consistency1.0
priorityRoute by server weight (higher = more traffic)Variable
randomRandom selection for load distribution0.5

Configuration

import { ToolRouter, type ToolRouterConfig } from "@juspay/neurolink";

const config: ToolRouterConfig = {
strategy: "least-loaded",
enableAffinity: true,
categoryMapping: {
database: ["db-primary", "db-replica"],
"file-system": ["fs-server"],
},
serverWeights: [
{ serverId: "db-primary", weight: 80, capabilities: ["database", "write"] },
{ serverId: "db-replica", weight: 20, capabilities: ["database", "read"] },
],
fallbackStrategy: "round-robin",
maxRetries: 3,
healthCheckInterval: 30000,
affinityTtl: 30 * 60 * 1000, // 30 minutes
};

const router = new ToolRouter(config);

Usage

// Register servers
router.registerServer("db-primary", ["database", "write"]);
router.registerServer("db-replica", ["database", "read"]);

// Route a tool call
const decision = router.route(
{ name: "queryUsers", category: "database" },
{ sessionId: "session-abc" },
);
// => { serverId: "db-primary", strategy: "least-loaded", confidence: 0.9, ... }

// Update server load after execution
router.updateServerLoad("db-primary", +1); // Request started
router.updateServerLoad("db-primary", -1); // Request completed

// Update health status
router.updateHealthStatus("db-replica", false); // Mark unhealthy

// Listen for events
router.on("routeDecision", ({ toolName, decision }) => {
console.log(`${toolName} -> ${decision.serverId}`);
});

// Get routing statistics
const stats = router.getStats();
// => { availableServers: 2, healthyServers: 1, activeAffinities: 1, serverLoads: {...} }

Annotation-Based Routing

The router automatically considers tool annotations when selecting servers:

  • Read-only tools -- Routed to any healthy server
  • Destructive tools -- Routed only to primary servers (weight >= 50)
  • Idempotent tools -- Prefer servers in the "caching" category
const candidates = router.routeByAnnotation({
name: "deleteUser",
annotations: { destructiveHint: true },
});
// Returns only high-weight primary servers

Default Configuration

import { DEFAULT_ROUTER_CONFIG } from "@juspay/neurolink";

// DEFAULT_ROUTER_CONFIG values:
// {
// strategy: "least-loaded",
// enableAffinity: true,
// maxRetries: 3,
// healthCheckInterval: 30000, // 30 seconds
// affinityTtl: 30 * 60 * 1000, // 30 minutes
// }

Events

ToolRouter extends EventEmitter and emits the following typed events (ToolRouterEvents):

EventPayloadFired When
routeDecision{ toolName: string, decision: RoutingDecision }A routing decision is made for a tool call
routeFailed{ toolName: string, error: Error, attemptedServers: string[] }Routing fails after exhausting all candidate servers
affinitySet{ key: string, serverId: string }A new session/user affinity rule is created
affinityExpired{ key: string }An affinity rule expires (TTL exceeded)
healthUpdate{ serverId: string, healthy: boolean }A server's health status changes
router.on("routeDecision", ({ toolName, decision }) => {
console.log(`${toolName} -> ${decision.serverId} (${decision.strategy})`);
});

router.on("healthUpdate", ({ serverId, healthy }) => {
console.log(`Server ${serverId} is now ${healthy ? "healthy" : "unhealthy"}`);
});

Additional Methods

MethodDescription
registerServer(serverId, capabilities?)Register a server as available for routing
unregisterServer(serverId)Remove a server from routing
route(tool, context?)Route a tool call to the best server
routeByCategory(tool, category)Get healthy servers for a category
routeByAnnotation(tool)Get servers based on tool annotation hints
routeByCapability(tool, requiredCapabilities)Get servers matching all required capabilities
updateServerLoad(serverId, delta)Adjust server load counter (+1 on start, -1 on end)
updateHealthStatus(serverId, healthy)Update server health; emits healthUpdate on change
setAffinity(key, serverId)Manually set session/user affinity
clearAffinity(key)Remove an affinity rule
getStats()Get routing statistics (servers, loads, affinities)
destroy()Stop affinity cleanup timer and clear all rules

Key Types

import type {
RoutingStrategy,
ToolRouterConfig,
RoutingDecision,
ServerWeight,
CategoryMapping,
AffinityRule,
ToolRouterEvents,
MCPTool,
} from "@juspay/neurolink";

Tool Cache

High-performance caching for MCP tool results with multiple eviction strategies, pattern-based invalidation, and cache-aside support.

Cache Strategies

StrategyDescriptionBest For
lruEvicts least recently accessed entriesGeneral use, temporal locality
fifoEvicts oldest entries firstStreaming data, time-sensitive
lfuEvicts least frequently used entriesStable workloads, popular items

Configuration

import { ToolCache, type CacheConfig } from "@juspay/neurolink";

const cache = new ToolCache<unknown>({
ttl: 5 * 60 * 1000, // 5 minutes
maxSize: 1000,
strategy: "lru",
enableAutoCleanup: true,
cleanupInterval: 60000,
namespace: "my-app",
});

Usage

// Basic set/get
cache.set("getUserById:123", { id: 123, name: "Alice" });
const user = cache.get("getUserById:123");

// Cache-aside pattern (recommended)
const result = await cache.getOrSet(
"getUserById:456",
async () => {
return await fetchUser(456); // Only called on cache miss
},
30000, // Optional: custom TTL for this entry
);

// Pattern-based invalidation (glob-style)
cache.invalidate("getUserById:*"); // Invalidate all user cache entries

// Generate cache keys from tool name + arguments
const key = ToolCache.generateKey("queryDatabase", {
table: "users",
limit: 10,
});

// Monitor cache performance
const stats = cache.getStats();
// => { hits: 42, misses: 8, evictions: 3, size: 47, maxSize: 1000, hitRate: 0.84 }

// Listen for cache events
cache.on("hit", ({ key }) => console.log(`Cache hit: ${key}`));
cache.on("evict", ({ key, reason }) =>
console.log(`Evicted: ${key} (${reason})`),
);

// Clean up
cache.destroy();

ToolResultCache

A specialized wrapper that automatically generates cache keys from tool name and arguments:

import { ToolResultCache } from "@juspay/neurolink";

const resultCache = new ToolResultCache({
ttl: 120000,
strategy: "lfu",
});

// Cache tool results directly
resultCache.cacheResult("getUserById", { id: 123 }, { name: "Alice" });
const cached = resultCache.getCachedResult("getUserById", { id: 123 });

// Invalidate all results for a specific tool
resultCache.invalidateTool("getUserById");

Default Configuration

import { DEFAULT_CACHE_CONFIG } from "@juspay/neurolink";

// DEFAULT_CACHE_CONFIG values:
// {
// ttl: 5 * 60 * 1000, // 5 minutes
// maxSize: 1000,
// strategy: "lru",
// enableAutoCleanup: true,
// cleanupInterval: 60000, // 1 minute
// }

Events

ToolCache extends EventEmitter and emits the following typed events (CacheEvents):

EventPayloadFired When
hit{ key: string, value: unknown }A cache lookup finds a valid (non-expired) entry
miss{ key: string }A cache lookup finds no entry or an expired entry
set{ key: string, value: unknown, ttl: number }A new entry is stored in the cache
evict{ key: string, reason: "expired" | "capacity" | "manual" }An entry is removed (expiry, capacity limit, or manual)
clear{ entriesRemoved: number }All entries are cleared from the cache

CacheStats Fields

The getStats() method returns a CacheStats object:

FieldTypeDescription
hitsnumberTotal cache hits since creation or last reset
missesnumberTotal cache misses since creation or last reset
evictionsnumberTotal evictions (expired + capacity + manual)
sizenumberCurrent number of entries in the cache
maxSizenumberMaximum capacity from configuration
hitRatenumberHit rate (0-1): hits / (hits + misses)

Additional Methods

MethodDescription
get(key)Get a value; returns undefined on miss or expiry
set(key, value, ttl?)Store a value with optional per-entry TTL override
has(key)Check if a key exists and is not expired
delete(key)Delete a specific key (emits evict with "manual")
invalidate(pattern)Delete entries matching a glob pattern (e.g. "user:*")
clear()Remove all entries
getOrSet(key, factory, ttl?)Cache-aside pattern: get or compute and cache
getStats()Get cache performance statistics
resetStats()Reset hit/miss/eviction counters
keys()Get all keys currently in the cache
sizeProperty: current entry count
ToolCache.generateKey(name, args)Static: generate a deterministic cache key
destroy()Stop auto-cleanup timer and clear all entries

Key Types

import type {
CacheConfig,
CacheStrategy,
CacheStats,
CacheEvents,
} from "@juspay/neurolink";

Request Batcher

Automatic batching of MCP tool calls for improved throughput. Groups requests by server, flushes based on batch size or timeout, and executes batches in parallel.

Configuration

import { RequestBatcher, type BatchConfig } from "@juspay/neurolink";

const batcher = new RequestBatcher<ToolResult>({
maxBatchSize: 10, // Flush when 10 requests are queued
maxWaitMs: 100, // Or after 100ms, whichever comes first
enableParallel: true, // Execute batch items in parallel
maxConcurrentBatches: 5, // Maximum batches in flight
groupByServer: true, // Group requests by server ID
});

Usage

// Set the batch executor
batcher.setExecutor(async (requests) => {
return Promise.all(
requests.map(async (r) => {
try {
const result = await executeToolCall(r.tool, r.args, r.serverId);
return { success: true, result };
} catch (error) {
return { success: false, error };
}
}),
);
});

// Add requests -- they are batched and flushed automatically
const [result1, result2, result3] = await Promise.all([
batcher.add("getUserById", { id: 1 }, "db-server"),
batcher.add("getUserById", { id: 2 }, "db-server"),
batcher.add("getOrder", { orderId: 99 }, "order-server"),
]);

// Manual flush
await batcher.flush();

// Wait for all pending requests
await batcher.drain();

// Check status
console.log(batcher.queueSize); // 0
console.log(batcher.isIdle); // true

// Clean up
batcher.destroy();

ToolCallBatcher

A higher-level wrapper designed specifically for MCP tool execution:

import { ToolCallBatcher } from "@juspay/neurolink";

const toolBatcher = new ToolCallBatcher({ maxBatchSize: 5, maxWaitMs: 50 });

toolBatcher.setToolExecutor(async (tool, args, serverId) => {
return await mcpClient.callTool(tool, args);
});

const result = await toolBatcher.execute("readFile", { path: "/data.json" });

Default Configuration

import { DEFAULT_BATCH_CONFIG } from "@juspay/neurolink";

// DEFAULT_BATCH_CONFIG values:
// {
// maxBatchSize: 10,
// maxWaitMs: 100, // 100 milliseconds
// enableParallel: true,
// maxConcurrentBatches: 5,
// groupByServer: true,
// }

Events

RequestBatcher extends EventEmitter and emits the following typed events (BatcherEvents):

EventPayloadFired When
batchStarted{ batchId: string, size: number }A batch begins execution
batchCompleted{ batchId: string, results: BatchResult<T>[] }A batch finishes executing all requests
batchFailed{ batchId: string, error: Error }A batch-level failure rejects all its requests
requestQueued{ requestId: string, queueSize: number }A new request is added to the queue
flushTriggered{ reason: "size" | "timeout" | "manual", queueSize: number }A flush is triggered (batch full, timer, or manual)
batcher.on("batchCompleted", ({ batchId, results }) => {
const successes = results.filter((r) => r.success).length;
console.log(`Batch ${batchId}: ${successes}/${results.length} succeeded`);
});

Additional Methods

MethodDescription
setExecutor(fn)Set the batch executor function
add(tool, args, serverId?)Add a request to the queue; returns a Promise
flush()Manually flush the current batch
drain()Flush and wait for all active batches to complete (30s timeout)
queueSizeProperty: number of pending requests
activeBatchCountProperty: number of batches currently in flight
isIdleProperty: true when no pending requests and no active batches
destroy()Reject all pending requests and stop the batcher

Key Types

import type {
BatchConfig,
BatchResult,
BatchExecutor,
BatcherEvents,
} from "@juspay/neurolink";

Tool Annotations

Safety metadata and behavior hints for MCP tools, implementing the MCP 2024-11-05 specification. Annotations guide AI models and middleware on how to handle tool execution.

Annotation Fields

FieldTypeDescription
readOnlyHintbooleanTool only reads data, no side effects
destructiveHintbooleanTool performs destructive operations
idempotentHintbooleanTool can be safely retried
requiresConfirmationbooleanTool needs user confirmation before running
titlestringHuman-readable title
tagsstring[]Custom tags for categorization
estimatedDurationnumberExpected execution time in milliseconds
rateLimitHintnumberSuggested calls per minute
costHintnumberRelative cost (arbitrary units)
complexitystring"simple", "medium", or "complex"
securityLevelstring"public", "internal", or "restricted"
openWorldHintbooleanTool may interact with external/open-world systems
auditRequiredbooleanTool execution should be audit-logged

Usage

import {
createAnnotatedTool,
inferAnnotations,
getToolSafetyLevel,
filterToolsByAnnotations,
requiresConfirmation,
isSafeToRetry,
validateAnnotations,
getAnnotationSummary,
mergeAnnotations,
} from "@juspay/neurolink";

// Create a tool with automatic annotation inference
const tool = createAnnotatedTool({
name: "deleteUser",
description: "Delete a user account permanently",
inputSchema: {
type: "object",
properties: { userId: { type: "string" } },
required: ["userId"],
},
execute: async (params) => {
/* ... */
},
});
// Annotations inferred: { destructiveHint: true, requiresConfirmation: true, complexity: "simple" }

// Check safety level
getToolSafetyLevel(tool); // => "dangerous"

// Check if confirmation is needed
requiresConfirmation(tool); // => true

// Check if safe to auto-retry on failure
isSafeToRetry(tool); // => false

// Get human-readable summary
getAnnotationSummary(tool.annotations);
// => "[DESTRUCTIVE | requires confirmation | simple complexity]"

// Filter tools by annotation predicates
const safeTools = filterToolsByAnnotations(
allTools,
(annotations) => annotations.readOnlyHint === true,
);

// Validate annotations for conflicts
const errors = validateAnnotations({
readOnlyHint: true,
destructiveHint: true, // Conflict!
});
// => ["Tool cannot be both readOnly and destructive - these are conflicting hints"]

// Merge multiple annotation objects (arrays like tags are merged, not overwritten)
const merged = mergeAnnotations(
{ readOnlyHint: true, tags: ["data"] },
{ idempotentHint: true, tags: ["safe"] },
);
// => { readOnlyHint: true, idempotentHint: true, tags: ["data", "safe"] }

Inference Heuristics

inferAnnotations analyzes tool names and descriptions to automatically assign hints:

  • Read-only: Names/descriptions containing get, list, read, fetch, query, search, find
  • Destructive: Names/descriptions containing delete, remove, drop, destroy, clear, purge
  • Idempotent: Names/descriptions containing set, update, put, upsert, replace
  • Complexity: Determined by keywords (analyze, process, generate = complex) and description length

Tool Converter

Bidirectional conversion between NeuroLink's internal tool format and the MCP protocol tool format, enabling interoperability with external MCP clients and servers.

Conversion Functions

import {
neuroLinkToolToMCP,
mcpToolToNeuroLink,
mcpProtocolToolToServerTool,
serverToolToMCPProtocol,
batchConvertToMCP,
batchConvertToNeuroLink,
createToolFromFunction,
validateToolName,
sanitizeToolName,
} from "@juspay/neurolink";

// Convert NeuroLink tool to MCP format
const mcpTool = neuroLinkToolToMCP(neuroLinkTool, {
inferAnnotations: true,
namespacePrefix: "myapp",
preserveMetadata: true,
});

// Convert MCP tool to NeuroLink format
const nlTool = mcpToolToNeuroLink(mcpServerTool, {
removeNamespacePrefix: "myapp",
});

// Batch convert
const mcpTools = batchConvertToMCP(neuroLinkTools);
const nlTools = batchConvertToNeuroLink(mcpServerTools);

// Create tool from a plain function
const tool = createToolFromFunction(
"calculateSum",
"Calculate the sum of two numbers",
async (params: { a: number; b: number }) => params.a + params.b,
{
parameters: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" },
},
required: ["a", "b"],
},
},
);

// Validate and sanitize tool names
const { valid, errors } = validateToolName("my-tool");
const safeName = sanitizeToolName("My Tool Name!"); // => "My_Tool_Name_"

Compatibility Matrix

import { TOOL_COMPATIBILITY } from "@juspay/neurolink";

console.log(TOOL_COMPATIBILITY.MCP_2024_11_05);
// { annotations: true, inputSchema: true, outputSchema: false, ... }

console.log(TOOL_COMPATIBILITY.NEUROLINK);
// { annotations: true, inputSchema: true, outputSchema: true, categories: true, tags: true, ... }

Key Types

import type {
NeuroLinkTool,
MCPProtocolTool,
ToolConverterOptions,
} from "@juspay/neurolink";

Tool Integration & Middleware

A middleware chain system for tool execution that integrates elicitation (interactive user input), confirmation flows, retry logic, timeouts, and logging.

Built-in Middleware

MiddlewareDescription
loggingMiddlewareLogs tool execution start, duration, and errors
confirmationMiddlewarePrompts user confirmation for destructive tools
validationMiddlewareValidates required parameters, elicits missing ones
createTimeoutMiddlewareWraps execution with a timeout
createRetryMiddlewareRetries failed calls for idempotent/read-only tools

Usage with ToolIntegrationManager

import {
ToolIntegrationManager,
loggingMiddleware,
confirmationMiddleware,
createTimeoutMiddleware,
createRetryMiddleware,
} from "@juspay/neurolink";

const manager = new ToolIntegrationManager();

// Set up elicitation handler (for interactive confirmation)
manager.setElicitationHandler(async (request) => {
if (request.type === "confirmation") {
const confirmed = await promptUser(request.message);
return {
requestId: request.id,
responded: true,
value: confirmed,
timestamp: Date.now(),
};
}
return { requestId: request.id, responded: false, timestamp: Date.now() };
});

// Add middleware chain
manager
.use(loggingMiddleware)
.use(confirmationMiddleware)
.use(createTimeoutMiddleware(30000))
.use(createRetryMiddleware(3, 1000));

// Register and execute tools
manager.registerTool(myTool);
const result = await manager.executeTool("myTool", { param: "value" });

Custom Middleware

import type { ToolMiddleware } from "@juspay/neurolink";

const metricsMiddleware: ToolMiddleware = async (
tool,
params,
context,
next,
) => {
const start = Date.now();
try {
const result = await next();
recordMetric(tool.name, Date.now() - start, "success");
return result;
} catch (error) {
recordMetric(tool.name, Date.now() - start, "error");
throw error;
}
};

Composable Middleware Chain

import {
createToolMiddlewareChain,
createElicitationContext,
globalToolIntegrationManager,
} from "@juspay/neurolink";

// Create a reusable middleware chain from an array of middlewares
const chain = createToolMiddlewareChain([
loggingMiddleware,
confirmationMiddleware,
createTimeoutMiddleware(30000),
]);

// Create elicitation context for middleware that needs interactive input
const elicitationCtx = createElicitationContext(elicitationManager);

// Use the pre-configured global singleton (has default middleware already wired)
globalToolIntegrationManager.registerTool(myTool);
const result = await globalToolIntegrationManager.executeTool("myTool", params);
ExportDescription
createToolMiddlewareChain(middlewares)Create composable middleware chain
createElicitationContext(manager)Create elicitation context for middleware
globalToolIntegrationManagerPre-configured singleton instance

Wrapping Individual Tools

import {
wrapToolWithElicitation,
wrapToolsWithElicitation,
} from "@juspay/neurolink";

const wrappedTool = wrapToolWithElicitation(tool, {
autoConfirmDestructive: false,
elicitationTimeout: 60000,
enableLogging: true,
});

// Batch wrap
const wrappedTools = wrapToolsWithElicitation(tools);

Key Types

import type {
ToolMiddleware,
EnhancedExecutionContext,
ToolWrapperOptions,
} from "@juspay/neurolink";

Enhanced Tool Discovery

Advanced tool search and filtering across multi-server environments with annotation awareness, category inference, compatibility checking, and safety-level grouping.

Usage

import { EnhancedToolDiscovery } from "@juspay/neurolink";

const discovery = new EnhancedToolDiscovery();

// Discover tools from a server with automatic annotation inference
const result = await discovery.discoverToolsWithAnnotations(
"github-server",
mcpClient,
10000, // timeout
);
console.log(`Found ${result.toolCount} tools`);

// Search with advanced criteria
const searchResult = discovery.searchTools({
name: "file",
category: "file-system",
annotations: { readOnlyHint: true },
sortBy: "name",
sortDirection: "asc",
limit: 10,
});

// Filter by safety level
const safeTools = discovery.getToolsBySafetyLevel("safe");
const dangerousTools = discovery.getToolsBySafetyLevel("dangerous");

// Get tools that require confirmation
const confirmationTools = discovery.getToolsRequiringConfirmation();

// Get tools for a specific server
const serverTools = discovery.getServerTools("github-server");

// Check tool compatibility
const compat = discovery.checkCompatibility("readFile", "fs-server", "2.0.0");
// => { compatible: true, issues: [], warnings: [], recommendations: [] }

// Get comprehensive statistics
const stats = discovery.getStatistics();
// => { totalTools, toolsByServer, toolsByCategory, toolsBySafetyLevel, ... }

Search Criteria

import type { ToolSearchCriteria } from "@juspay/neurolink";

const criteria: ToolSearchCriteria = {
name: "query", // Partial name match
description: "database", // Keyword match in description
serverIds: ["db-server-1"], // Filter by servers
category: "database", // Filter by inferred category
tags: ["sql"], // Filter by annotation tags
annotations: { readOnlyHint: true }, // Filter by annotation flags
includeUnavailable: false, // Exclude offline tools
sortBy: "successRate", // Sort field
sortDirection: "desc", // Sort direction
limit: 20, // Max results
};

Events

EnhancedToolDiscovery extends EventEmitter and emits the following events:

EventPayloadFired When
toolDiscovered{ serverId: string, toolName: string, annotations: MCPToolAnnotations, timestamp: Date }A tool is discovered from a server
annotationsUpdated{ serverId: string, toolName: string, annotations: MCPToolAnnotations, timestamp: Date }Tool annotations are manually updated

Additional Methods

MethodDescription
discoverToolsWithAnnotations(serverId, client, timeout?)Discover tools from a server with auto-inferred annotations
searchTools(criteria)Search tools with advanced filtering criteria
getToolsBySafetyLevel(level)Get tools by safety level: "safe", "moderate", "dangerous"
getToolsRequiringConfirmation()Get tools that require user confirmation
getReadOnlyTools()Get all read-only tools
checkCompatibility(toolName, serverId, targetVersion?)Check tool version and feature compatibility
getTool(serverId, toolName)Get a specific tool by server and name
getAllTools()Get all registered tools across all servers
getServerTools(serverId)Get all tools for a specific server
updateToolAnnotations(serverId, toolName, annotations)Update tool annotations manually
registerServer(server)Register a server with the internal multi-server manager
getUnifiedTools()Get unified tool list from all servers
getStatistics()Get comprehensive statistics by server, category, safety

Key Types

import type {
EnhancedToolInfo,
ToolSearchCriteria,
ToolSearchResult,
CompatibilityCheckResult,
} from "@juspay/neurolink";

Elicitation Protocol

The elicitation system enables MCP tools to request interactive user input mid-execution. This is critical for human-in-the-loop (HITL) workflows such as confirming destructive operations, requesting missing parameters, or handling authentication challenges.

Elicitation Types

TypeDescriptionResponse Type
confirmationYes/no confirmation dialogboolean
textFree text inputstring
selectSingle selection from optionsstring
multiselectMultiple selection from optionsstring[]
formStructured form with fieldsRecord<string, unknown>
fileFile selection/uploadFile reference
secretSensitive input (passwords)string

ElicitationManager

import { ElicitationManager } from "@juspay/neurolink";

const manager = new ElicitationManager({
defaultTimeout: 60000,
enabled: true,
fallbackBehavior: "timeout", // "timeout" | "default" | "error"
handler: async (request) => {
// Implement your UI prompt based on request type
switch (request.type) {
case "confirmation": {
const confirmed = await showConfirmDialog(request.message);
return {
requestId: request.id,
responded: true,
value: confirmed,
timestamp: Date.now(),
};
}
case "text": {
const text = await showTextInput(request.message);
return {
requestId: request.id,
responded: true,
value: text,
timestamp: Date.now(),
};
}
default:
return {
requestId: request.id,
responded: false,
timestamp: Date.now(),
};
}
},
});

// Convenience methods
const confirmed = await manager.confirm("Delete this file?", {
toolName: "deleteFile",
confirmLabel: "Yes, delete",
cancelLabel: "Cancel",
});

const name = await manager.getText("Enter project name:", {
placeholder: "my-project",
defaultValue: "untitled",
});

const choice = await manager.select("Select environment:", [
{ value: "dev", label: "Development" },
{ value: "staging", label: "Staging" },
{ value: "prod", label: "Production" },
]);

const formData = await manager.form("Configure deployment:", [
{
name: "region",
label: "Region",
type: "select",
required: true,
options: [
{ value: "us-east-1", label: "US East" },
{ value: "eu-west-1", label: "EU West" },
],
},
{
name: "replicas",
label: "Replicas",
type: "number",
required: true,
defaultValue: 3,
},
]);

// Request secret/password input (masked)
const apiKey = await manager.getSecret("Enter API key:", {
toolName: "configureService",
});

// Multi-selection from options
const regions = await manager.multiSelect(
"Select deployment regions:",
[
{ value: "us-east-1", label: "US East" },
{ value: "eu-west-1", label: "EU West" },
{ value: "ap-south-1", label: "AP South" },
],
{ minSelections: 1, maxSelections: 2 },
);

// Cancel a pending request
manager.cancel("request-id-123", "User navigated away");

// Enable/disable elicitation
manager.setEnabled(false);
console.log(manager.isEnabled()); // => false

// Inspect pending requests
console.log(manager.getPendingCount()); // => 0
const pending = manager.getPendingRequests();
manager.clearPending("Session ended");

Additional Methods

MethodDescription
confirm(message, options?)Yes/no confirmation dialog
getText(message, options?)Free text input
select(message, options, config?)Single selection from options
multiSelect(message, options, config?)Multi-selection from options
form(message, fields)Structured form with fields
getSecret(message, options?)Request secret/password input
cancel(requestId, reason?)Cancel pending request
setEnabled(enabled)Enable/disable elicitation
isEnabled()Check if enabled
getPendingCount()Get pending request count
getPendingRequests()Get all pending requests
clearPending(reason?)Clear all pending requests

ElicitationProtocolAdapter

Bridges protocol-level JSON-RPC 2.0 messages with the ElicitationManager for cross-transport communication:

import {
ElicitationProtocolAdapter,
createConfirmationRequest,
createTextInputRequest,
isElicitationProtocolMessage,
} from "@juspay/neurolink";

const adapter = new ElicitationProtocolAdapter({
defaultTimeout: 30000,
enableLogging: true,
});

// Handle incoming protocol messages
const response = await adapter.handleMessage(incomingMessage);

// Create protocol-compliant request messages
const confirmMsg = createConfirmationRequest("Are you sure?", {
toolName: "dropTable",
confirmLabel: "Drop it",
cancelLabel: "Keep",
timeout: 15000,
});

// Check if a message is an elicitation protocol message
if (isElicitationProtocolMessage(msg)) {
await adapter.handleMessage(msg);
}

Events

ElicitationManager extends EventEmitter and emits the following events:

EventPayloadFired When
elicitationRequestedElicitation (the full request object)A new elicitation request is created
elicitationRespondedElicitationResponseThe handler successfully responds to a request
elicitationError{ request: Elicitation, error: unknown }The handler throws an error
elicitationTimeout{ request: Elicitation }A request times out before receiving a response
elicitationCancelled{ requestId: string, reason?: string }A pending request is manually cancelled
manager.on("elicitationRequested", (request) => {
console.log(`Elicitation requested: ${request.type} for ${request.toolName}`);
});

manager.on("elicitationTimeout", ({ request }) => {
console.log(`Request ${request.id} timed out`);
});

Key Types

import type {
ElicitationType,
Elicitation,
ElicitationResponse,
ElicitationHandler,
ElicitationManagerConfig,
ElicitationContext,
FormField,
SelectOption,
ElicitationProtocolMessage,
ElicitationProtocolPayload,
} from "@juspay/neurolink";

Multi-Server Manager

Coordinates multiple MCP servers with load balancing, failover, unified tool discovery, and server grouping.

Load Balancing Strategies

StrategyDescription
round-robinRotate through servers sequentially
least-loadedPrefer server with fewest active requests
randomRandom selection
weightedWeighted random based on server priority
failover-onlyUse primary server, failover only on failure

Usage

import { MultiServerManager } from "@juspay/neurolink";

const manager = new MultiServerManager({
defaultStrategy: "least-loaded",
healthAwareRouting: true,
healthCheckInterval: 30000,
maxFailoverRetries: 3,
autoNamespace: true,
namespaceSeparator: ".",
conflictResolution: "first-wins",
});

// Add servers
manager.addServer({ id: "db-primary", name: "DB Primary", status: "connected", tools: [...] });
manager.addServer({ id: "db-replica", name: "DB Replica", status: "connected", tools: [...] });

// Create server groups for load balancing
manager.createGroup({
id: "database-pool",
name: "Database Servers",
servers: ["db-primary", "db-replica"],
strategy: "least-loaded",
healthAware: true,
weights: [
{ serverId: "db-primary", weight: 70, priority: 0 },
{ serverId: "db-replica", weight: 30, priority: 1 },
],
});

// Select a server for a tool call
const selection = manager.selectServer("queryUsers", "database-pool");
if (selection) {
console.log(`Using server: ${selection.serverId}`);
}

// Get unified tool list across all servers
const tools = manager.getUnifiedTools();
for (const tool of tools) {
console.log(`${tool.name} - available on ${tool.servers.length} server(s)`);
if (tool.hasConflict) {
console.log(" WARNING: Tool name conflict across servers");
}
}

// Track request metrics
manager.requestStarted("db-primary");
// ... execute request ...
manager.requestCompleted("db-primary", 150, true);

// Get namespaced tools (server.toolName format)
const nsTools = manager.getNamespacedTools();
// => [{ fullName: "db-primary.queryUsers", toolName: "queryUsers", ... }]

Methods

MethodDescription
addServer(server)Add a server to the manager
removeServer(serverId)Remove a server from the manager
updateServer(serverId, updates)Update server configuration
getServers()Get all servers
getServer(serverId)Get specific server
createGroup(group)Create a server group
removeGroup(groupId)Remove a server group
addServerToGroup(serverId, groupId)Add server to group
removeServerFromGroup(serverId, groupId)Remove server from group
getGroups()Get all groups
getGroup(groupId)Get specific group
selectServer(toolName, groupId?)Select a server for a tool call
setToolPreference(toolName, serverId)Set preferred server for a tool
clearToolPreference(toolName)Clear tool preference
getUnifiedTools()Get unified tool list across all servers
getNamespacedTools()Get tools with server namespace prefixes
requestStarted(serverId)Track request start for load balancing
requestCompleted(serverId, duration, success)Track request completion
getServerMetrics(serverId)Get server metrics
getAllMetrics()Get all metrics

Events

MultiServerManager extends EventEmitter and emits the following events:

EventPayloadFired When
serverAdded{ serverId: string, server: MCPServerInfo }A server is added to the manager
serverRemoved{ serverId: string }A server is removed from the manager
serverUpdated{ serverId: string, server: MCPServerInfo }Server info is updated
groupCreated{ group: ServerGroup }A new server group is created
groupRemoved{ groupId: string }A server group is removed
serverAddedToGroup{ serverId: string, groupId: string }A server is added to a group
serverRemovedFromGroup{ serverId: string, groupId: string }A server is removed from a group
metricsUpdated{ serverId: string, metrics: ServerMetrics }Server metrics are updated
toolPreferenceSet{ toolName: string, serverId: string }A tool routing preference is set

Key Types

import type {
LoadBalancingStrategy,
MultiServerManagerConfig,
ServerGroup,
ServerWeight,
UnifiedTool,
} from "@juspay/neurolink";

MCP Server Base

Abstract base class for building custom MCP servers with consistent patterns for tool registration, execution, and lifecycle management.

Creating a Custom Server

import { MCPServerBase, type MCPServerTool } from "@juspay/neurolink";

class DataProcessingServer extends MCPServerBase {
constructor() {
super({
id: "data-processing",
name: "Data Processing Server",
description: "Provides data transformation and analysis tools",
version: "1.0.0",
category: "data",
defaultAnnotations: {
securityLevel: "internal",
},
});

// Register tools
this.registerTool({
name: "transformCSV",
description: "Transform CSV data with column mapping",
annotations: {
readOnlyHint: true,
idempotentHint: true,
estimatedDuration: 5000,
},
inputSchema: {
type: "object",
properties: {
data: { type: "string", description: "CSV content" },
mapping: { type: "object", description: "Column mapping" },
},
required: ["data"],
},
execute: async (params) => {
const { data, mapping } = params as { data: string; mapping?: object };
const transformed = await this.processCSV(data, mapping);
return { success: true, data: transformed };
},
});
}

protected async onInit(): Promise<void> {
// Async initialization (load configs, warm caches, etc.)
}

protected async onStart(): Promise<void> {
// Start background tasks
}

protected async onStop(): Promise<void> {
// Clean up resources
}

private async processCSV(data: string, mapping?: object): Promise<string> {
// Implementation
return data;
}
}

// Usage
const server = new DataProcessingServer();
await server.init();
await server.start();

// Execute tools
const result = await server.executeTool("transformCSV", { data: "a,b\n1,2" });

// Convert to MCPServerInfo for registration with MultiServerManager
const serverInfo = server.toServerInfo();

// Query tools by annotation
const readOnlyTools = server.getReadOnlyTools();
const destructiveTools = server.getDestructiveTools();

// Lifecycle events
server.on("toolRegistered", ({ toolName }) =>
console.log(`Registered: ${toolName}`),
);
server.on("toolExecuted", ({ toolName, duration }) =>
console.log(`${toolName}: ${duration}ms`),
);

Additional Methods

MethodDescription
init()Run onInit() lifecycle hook
start()Start the server (calls onStart())
stop()Stop the server (calls onStop())
registerTool(tool)Register a tool with the server
registerTools(tools)Register multiple tools at once
executeTool(name, params)Execute a registered tool by name
getTools()Get all registered tools
getTool(name)Get a specific tool by name
hasTool(name)Check if tool exists
removeTool(name)Remove a tool
toServerInfo()Convert server state to MCPServerInfo for registration
getToolsByAnnotation(annotation, value)Filter tools by a specific annotation key and value
getReadOnlyTools()Get tools with readOnlyHint: true
getDestructiveTools()Get tools with destructiveHint: true
getIdempotentTools()Get tools with idempotentHint: true
getToolsRequiringConfirmation()Get tools with requiresConfirmation: true

Events

MCPServerBase extends EventEmitter and emits the following typed events (MCPServerEvents):

EventPayloadFired When
toolRegistered{ toolName: string, tool: MCPServerTool }A tool is registered with the server
toolExecuted{ toolName: string, duration: number, success: boolean }A tool finishes execution (success or failure)
toolError{ toolName: string, error: Error }A tool throws an error during execution
serverReady{ tools: string[] }The server finishes initialization
serverStopped{ reason?: string }The server is stopped

Key Types

import type {
MCPServerBaseConfig,
MCPServerEvents,
MCPServerTool,
MCPToolAnnotations,
} from "@juspay/neurolink";

Agent & Workflow Exposure

Expose NeuroLink agents and workflows as MCP tools, allowing external MCP clients to invoke complex AI operations through the standardized MCP protocol.

Exposing Agents

import {
exposeAgentAsTool,
exposeAgentsAsTools,
AgentExposureManager,
type ExposableAgent,
} from "@juspay/neurolink";

const agent: ExposableAgent = {
id: "support-agent",
name: "Customer Support Agent",
description: "Handles customer support queries with knowledge base lookup",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Customer question" },
customerId: { type: "string" },
},
required: ["query"],
},
execute: async (input) => {
// Agent execution logic
return { answer: "...", confidence: 0.95 };
},
metadata: {
version: "2.1.0",
category: "support",
tags: ["customer-service", "knowledge-base"],
estimatedDuration: 5000,
},
};

// Expose as a single MCP tool
const { tool, toolName } = exposeAgentAsTool(agent, {
prefix: "agent",
executionTimeout: 300000,
enableLogging: true,
});
// => toolName: "agent_customer_support_agent"

Exposing Workflows

import {
exposeWorkflowAsTool,
type ExposableWorkflow,
} from "@juspay/neurolink";

const workflow: ExposableWorkflow = {
id: "onboarding-flow",
name: "User Onboarding",
description: "Complete user onboarding workflow",
steps: [
{ id: "validate", name: "Validate Input" },
{ id: "create-account", name: "Create Account" },
{ id: "send-welcome", name: "Send Welcome Email" },
],
inputSchema: {
type: "object",
properties: {
email: { type: "string" },
name: { type: "string" },
},
required: ["email", "name"],
},
execute: async (input) => {
return { userId: "user-123", status: "onboarded" };
},
metadata: {
version: "1.0.0",
idempotent: true,
estimatedDuration: 15000,
},
};

const { tool } = exposeWorkflowAsTool(workflow, {
prefix: "workflow",
executionTimeout: 600000,
});

AgentExposureManager

Manages the lifecycle of all exposed agents and workflows:

const exposureManager = new AgentExposureManager({
prefix: "neurolink",
enableLogging: true,
});

exposureManager.exposeAgent(supportAgent);
exposureManager.exposeWorkflow(onboardingWorkflow);

const allTools = exposureManager.getExposedTools();
const agentTools = exposureManager.getToolsBySourceType("agent");
const stats = exposureManager.getStatistics();
// => { totalExposed: 2, exposedAgents: 1, exposedWorkflows: 1, toolNames: [...] }

ExposureOptions

FieldTypeDefaultDescription
prefixstring"agent"/"workflow"Prefix for generated tool names
defaultAnnotationsMCPToolAnnotations{}Annotations applied to all exposed tools
includeMetadataInDescriptionbooleantrueAppend source metadata to tool description
nameTransformer(name: string) => stringlowercase + _Transform source name to MCP tool name
wrapWithContextbooleantrueWrap execution with context (logging, timeout)
executionTimeoutnumber300000 (agent) / 600000 (workflow)Timeout in ms
enableLoggingbooleantrueLog execution start/end/errors

ExposureResult

FieldTypeDescription
toolMCPServerToolThe generated MCP tool
sourceType"agent" | "workflow"Whether source is agent or workflow
sourceIdstringOriginal agent/workflow ID
toolNamestringGenerated MCP tool name

Additional Methods (AgentExposureManager)

MethodDescription
exposeAgent(agent)Expose an agent and register the tool
exposeWorkflow(workflow)Expose a workflow and register the tool
getExposedTools()Get all exposed tools as MCPServerTool[]
getExposureResult(toolName)Get the ExposureResult for a tool name
getToolsBySourceType(type)Get tools filtered by "agent" or "workflow"
unexpose(toolName)Remove a single exposed tool; returns boolean
clear()Remove all exposed tools
getStatistics()Get counts: totalExposed, agents, workflows

Key Types

import type {
ExposableAgent,
ExposableWorkflow,
ExposureOptions,
ExposureResult,
} from "@juspay/neurolink";

Server Capabilities

Manages resources and prompts for MCP servers according to the MCP specification. Enables servers to expose data as resources and reusable prompt templates.

Resources

import {
ServerCapabilitiesManager,
createTextResource,
createJsonResource,
} from "@juspay/neurolink";

const capabilities = new ServerCapabilitiesManager({
resources: true,
prompts: true,
resourceSubscriptions: true,
});

// Register a static text resource
capabilities.registerResource(
createTextResource(
"config://app/settings",
"Application Settings",
"key=value\nport=3000",
{ description: "Current application settings" },
),
);

// Register a dynamic JSON resource
capabilities.registerResource(
createJsonResource(
"data://metrics/current",
"Current Metrics",
async () => ({ cpu: 45, memory: 72, requests: 1250 }),
{ description: "Live system metrics", dynamic: true },
),
);

// Register a resource template for pattern-matched URIs
capabilities.registerResourceTemplate("data://users/{id}", {
name: "User Data",
uriPattern: "data://users/{id}",
mimeType: "application/json",
reader: async (uri) => {
const id = uri.split("/").pop();
return {
uri,
mimeType: "application/json",
text: JSON.stringify({ id, name: "User" }),
};
},
});

// Read a resource
const content = await capabilities.readResource("config://app/settings");

// Subscribe to resource changes
const unsubscribe = capabilities.subscribeToResource(
"data://metrics/current",
(uri, content) => {
console.log(`Metrics updated:`, content.text);
},
);

// Notify subscribers of a change
await capabilities.notifyResourceChanged("data://metrics/current");

// Unsubscribe
unsubscribe();

Prompts

import { createPrompt } from "@juspay/neurolink";

// Register a simple template-based prompt
capabilities.registerPrompt(
createPrompt(
"summarize",
"Please summarize the following text concisely:\n\n{text}",
{
description: "Summarize text content",
arguments: [
{ name: "text", description: "Text to summarize", required: true },
],
},
),
);

// Register a prompt with a custom generator
capabilities.registerPrompt({
name: "code-review",
description: "Review code for issues and improvements",
arguments: [
{ name: "code", description: "Source code to review", required: true },
{ name: "language", description: "Programming language" },
],
generator: async (args) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Review this ${args.language || "code"} for bugs, security issues, and improvements:\n\n${args.code}`,
},
},
],
}),
});

// Generate a prompt
const result = await capabilities.getPrompt("summarize", {
text: "NeuroLink is an enterprise AI platform...",
});

// List all prompts and resources
const prompts = capabilities.listPrompts();
const resources = capabilities.listResources();

// Get MCP capabilities object for protocol negotiation
const caps = capabilities.getCapabilities();
// => { resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true } }

Additional Methods (ServerCapabilitiesManager)

MethodDescription
registerResource(resource)Register a static or dynamic resource
registerResourceTemplate(pattern, template)Register a URI-template resource for pattern matching
readResource(uri)Read a resource by URI (resolves templates)
listResources()List all registered resources as MCPResource[]
getResource(uri)Get a registered resource by URI
subscribeToResource(uri, callback)Subscribe to resource changes; returns unsubscribe fn
notifyResourceChanged(uri)Notify all subscribers that a resource has changed
registerPrompt(prompt)Register a prompt with static template or async generator
getPrompt(name, args?)Generate a prompt result with provided arguments
listPrompts()List all registered prompts as MCPPrompt[]
getCapabilities()Get MCP capabilities object for protocol negotiation

Events

ServerCapabilitiesManager extends EventEmitter and emits the following events:

EventPayloadFired When
resourceRegistered{ uri: string, name: string, timestamp: Date }A resource is registered
resourceTemplateRegistered{ pattern: string, timestamp: Date }A resource template is registered
resourceUnregistered{ uri: string, timestamp: Date }A resource is unregistered
resourceRead{ uri: string, duration: number, success: boolean, timestamp: Date, error?: string }A resource is read (success or failure)
resourceSubscribed{ uri: string, timestamp: Date }A subscription is added to a resource
resourceUnsubscribed{ uri: string, timestamp: Date }A subscription is removed from a resource
resourceChanged{ uri: string, subscriberCount: number, timestamp: Date }A resource change is notified to subscribers
promptRegistered{ name: string, timestamp: Date }A prompt is registered
promptUnregistered{ name: string, timestamp: Date }A prompt is unregistered
promptGenerated{ name: string, duration: number, success: boolean, messageCount?: number, timestamp: Date, error?: string }A prompt is generated
cleared{ timestamp: Date }All resources and prompts are cleared

Additional Methods

MethodDescription
registerResource(resource)Register a resource with a reader function
registerResourceTemplate(pattern, template)Register a URI-pattern-based resource template
unregisterResource(uri)Remove a resource and its subscriptions
listResources()List all registered resources
readResource(uri, context?)Read a resource by URI (checks templates on miss)
getResource(uri)Get resource definition by URI
subscribeToResource(uri, callback)Subscribe to resource changes; returns unsubscribe function
notifyResourceChanged(uri)Read resource and notify all subscribers
registerPrompt(prompt)Register a prompt with a generator function
unregisterPrompt(name)Remove a prompt
listPrompts()List all registered prompts
getPrompt(name, args?, context?)Generate a prompt with arguments
getPromptDefinition(name)Get prompt definition without generating
getCapabilities()Get MCP capabilities object for protocol negotiation
getStatistics()Get counts of resources, templates, prompts, subscriptions
clear()Clear all resources, templates, prompts, and subscriptions

Key Types

import type {
MCPResource,
ResourceContent,
ResourceReader,
RegisteredResource,
MCPPrompt,
PromptMessage,
PromptResult,
PromptGenerator,
RegisteredPrompt,
ServerCapabilitiesConfig,
ResourceSubscriptionCallback,
} from "@juspay/neurolink";

MCP Registry Client

Discover MCP servers from registries, including a built-in catalog of well-known servers. Search by category, tags, transport type, and verification status.

Usage

import {
MCPRegistryClient,
getWellKnownServer,
getAllWellKnownServers,
} from "@juspay/neurolink";

const client = new MCPRegistryClient({
enableCache: true,
defaultCacheTTL: 3600000, // 1 hour
});

// Search for servers
const results = await client.search({
query: "database",
categories: ["database"],
verifiedOnly: true,
sortBy: "downloads",
limit: 10,
});

for (const entry of results.entries) {
console.log(`${entry.name} - ${entry.description}`);
console.log(` Install: ${client.getInstallCommand(entry)}`);

// Check if required env vars are set
const { ready, missing } = client.checkRequiredEnvVars(entry);
if (!ready) {
console.log(` Missing: ${missing.join(", ")}`);
}
}

// Get a specific well-known server
const postgres = getWellKnownServer("postgres");
// => { id: "postgres", name: "PostgreSQL", npmPackage: "@modelcontextprotocol/server-postgres", ... }

// Convert registry entry to MCPServerInfo for NeuroLink
const serverInfo = client.toServerInfo(postgres);

// Browse by category
const dbServers = await client.getByCategory("database");
const searchServers = await client.getByTag("search");

// Get all available categories and tags
const categories = await client.getCategories();
const tags = await client.getTags();

// Add custom registry entries
client.addCustomEntry({
id: "my-internal-server",
name: "Internal Data Server",
description: "Company internal data API",
version: "3.0.0",
command: "node",
args: ["./servers/data-server.js"],
categories: ["data", "internal"],
verified: false,
});

Methods

MethodDescription
search(options)Search for servers with filters
getByCategory(category)Browse servers by category
getByTag(tag)Filter entries by tag
getCategories()Get all available categories
getTags()Get all available tags
getEntry(id)Get a specific registry entry
getAllEntries()Get all entries from all registries
addCustomEntry(entry)Add a custom registry entry
removeCustomEntry(id)Remove a custom entry
addRegistry(config)Add a custom registry source
getPopularServers(limit?)Get popular servers
getVerifiedServers()Get verified servers
getStatistics()Get registry statistics
toServerInfo(entry)Convert entry to MCPServerInfo
getInstallCommand(entry)Get install command for an entry
checkRequiredEnvVars(entry)Check if required env vars are set

Well-Known Servers

The registry includes these verified servers out of the box:

IDNameCategoriesKey Tools
filesystemFilesystemfile-systemread_file, write_file, list_dir
githubGitHubversion-control, apicreate_repo, list_commits
postgresPostgreSQLdatabasequery, list_tables
sqliteSQLitedatabasequery, list_tables
brave-searchBrave Searchsearch, apiweb_search, local_search
puppeteerPuppeteerautomation, webnavigate, screenshot, click
gitGitversion-controlgit_status, git_log, git_diff
memoryMemorymemory, storagestore, retrieve, search
slackSlackcommunication, apisend_message, list_channels
google-driveGoogle Drivefile-system, apilist_files, read_file

Key Types

import type {
MCPRegistryClientConfig,
RegistryConfig,
RegistryEntry,
RegistrySearchOptions,
RegistrySearchResult,
RegistrySourceType,
} from "@juspay/neurolink";

Architecture

For detailed per-module flow diagrams (Tool Router decision flow, Tool Cache eviction, Request Batcher sequencing, Elicitation Protocol handshake, and Multi-Server topology), see MCP Enhancement Architecture Diagrams.

The MCP enhancement modules are layered on top of NeuroLink's existing MCP infrastructure:

graph TB
subgraph "Application Layer"
A[NeuroLink SDK] --> B[MCP Tool Registry]
end

subgraph "MCP Enhancements"
B --> C[Tool Router]
B --> D[Tool Cache]
B --> E[Request Batcher]
B --> F[Tool Integration]

C --> G[Multi-Server Manager]
F --> H[Elicitation Manager]
H --> I[Elicitation Protocol]

J[Enhanced Tool Discovery] --> G
J --> K[Tool Annotations]
K --> C

L[MCP Server Base] --> M[Agent Exposure]
L --> N[Server Capabilities]

O[MCP Registry Client] --> G
end

subgraph "MCP Servers"
G --> P[Server 1]
G --> Q[Server 2]
G --> R[Server N]
end

Data Flow

sequenceDiagram
participant App as Application
participant TI as Tool Integration
participant TR as Tool Router
participant TC as Tool Cache
participant RB as Request Batcher
participant S as MCP Server

App->>TI: Execute tool call
TI->>TI: Run middleware chain
TI->>TC: Check cache
alt Cache hit
TC-->>App: Return cached result
else Cache miss
TC->>TR: Route to server
TR->>TR: Select server (strategy)
TR->>RB: Queue request
RB->>S: Execute batch
S-->>RB: Return results
RB-->>TC: Cache result
TC-->>App: Return result
end

End-to-End Integration Example

This example shows how ToolCache, Tool Annotations, ToolIntegration middleware, and MCPServerBase compose together in a realistic scenario: a custom MCP server whose tools are executed through a middleware pipeline with caching, retry, timeout, and confirmation for destructive operations.

import {
MCPServerBase,
ToolCache,
ToolIntegrationManager,
createAnnotatedTool,
loggingMiddleware,
confirmationMiddleware,
createTimeoutMiddleware,
createRetryMiddleware,
requiresConfirmation,
isSafeToRetry,
type MCPServerTool,
} from "@juspay/neurolink";

// -------------------------------------------------------
// 1. Build a custom MCP server with annotated tools
// -------------------------------------------------------

class InventoryServer extends MCPServerBase {
constructor() {
super({
id: "inventory",
name: "Inventory Server",
description: "Product inventory management tools",
version: "1.0.0",
category: "database",
});

// Read-only tool -- annotations are inferred automatically
this.registerTool(
createAnnotatedTool({
name: "getProduct",
description: "Get product details by SKU",
inputSchema: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
},
execute: async (params) => {
const { sku } = params as { sku: string };
// Simulate a database lookup
return { sku, name: "Widget", stock: 42, price: 9.99 };
},
}),
);

// Destructive tool -- inferred as destructive, requires confirmation
this.registerTool(
createAnnotatedTool({
name: "deleteProduct",
description: "Permanently delete a product from inventory",
inputSchema: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
},
execute: async (params) => {
const { sku } = params as { sku: string };
return { deleted: true, sku };
},
}),
);
}
}

// -------------------------------------------------------
// 2. Wire up caching, middleware, and the server
// -------------------------------------------------------

async function main() {
// Start the custom server
const server = new InventoryServer();
await server.init();
await server.start();

// Create a cache for read-only tool results
const cache = new ToolCache({
ttl: 5 * 60 * 1000, // 5 minutes
maxSize: 200,
strategy: "lru",
});

// Set up middleware pipeline: logging -> confirmation -> timeout -> retry
const integration = new ToolIntegrationManager();

integration.setElicitationHandler(async (request) => {
if (request.type === "confirmation") {
// In production, prompt the user via your UI framework.
// Here we auto-approve for demonstration purposes.
console.log(\`[Confirmation] \${request.message} -> auto-approved\`);
return {
requestId: request.id,
responded: true,
value: true,
timestamp: Date.now(),
};
}
return { requestId: request.id, responded: false, timestamp: Date.now() };
});

integration
.use(loggingMiddleware)
.use(confirmationMiddleware)
.use(createTimeoutMiddleware(30_000))
.use(createRetryMiddleware(3, 1000));

// Register server tools with the middleware manager
for (const tool of server.listTools()) {
integration.registerTool(tool);
}

// -------------------------------------------------------
// 3. Execute tools through the integrated pipeline
// -------------------------------------------------------

// Read-only call: cached + retryable, no confirmation needed
const getProduct = server.listTools().find((t) => t.name === "getProduct")!;
console.log("Requires confirmation:", requiresConfirmation(getProduct)); // false
console.log("Safe to retry:", isSafeToRetry(getProduct)); // true

const productResult = await cache.getOrSet(
ToolCache.generateKey("getProduct", { sku: "W-100" }),
() => integration.executeTool("getProduct", { sku: "W-100" }),
);
console.log("Product:", productResult);

// Second call hits cache
const cachedResult = cache.get(
ToolCache.generateKey("getProduct", { sku: "W-100" }),
);
console.log("From cache:", cachedResult !== undefined);
console.log("Cache stats:", cache.getStats());

// Destructive call: triggers confirmation middleware, skips cache
const deleteResult = await integration.executeTool("deleteProduct", {
sku: "W-100",
});
console.log("Delete result:", deleteResult);

// Invalidate cache for the deleted product
cache.invalidate("getProduct:*");

// -------------------------------------------------------
// 4. Clean up
// -------------------------------------------------------
cache.destroy();
await server.stop();
}

main().catch(console.error);

What this demonstrates:

  • MCPServerBase provides a structured way to define and register tools with lifecycle hooks (`init`, `start`, `stop`).
  • Tool Annotations are inferred automatically from tool names and descriptions -- `getProduct` is read-only, `deleteProduct` is destructive.
  • ToolIntegrationManager chains middleware so every tool call passes through logging, confirmation (for destructive tools), timeout, and retry (for idempotent/read-only tools).
  • ToolCache wraps read-only calls with `getOrSet` to avoid redundant execution, and `invalidate` clears stale entries after mutations.

See the Architecture Diagrams for visual flows of how these components interact.


Error Handling

All MCP enhancement modules use ErrorFactory from @juspay/neurolink for consistent, typed errors. Errors include a descriptive message and often a hint field with a suggested fix.

Error Types by Module

ModuleErrorFactory MethodWhen Thrown
ToolRouter(none -- returns empty array)Returns empty candidates array instead of throwing
ToolCache(none -- returns undefined)Returns undefined on miss; eviction events carry reason
RequestBatcherinvalidConfiguration()maxBatchSize < 1 or maxWaitMs < 0
RequestBatchermissingConfiguration()setExecutor() not called before add() or flush()
RequestBatchertoolTimeout()drain() exceeds 30-second timeout
ToolCallBatchermissingConfiguration()setToolExecutor() not called before execute()
MCPServerBaseinvalidConfiguration()Missing required config (id, name, version, etc.)
MCPServerBasetoolTimeout()Tool execution exceeds timeout
MultiServerManagerinvalidConfiguration()Duplicate server ID, unknown server in group, unknown group
EnhancedToolDiscoverytoolExecutionFailed()Discovery fails for a server
ToolIntegrationtoolTimeout()Timeout middleware expires
ToolIntegrationtoolNotFound()Tool not registered in the integration manager
ServerCapabilitiesinvalidConfiguration()Resources/prompts disabled, duplicate URI/name, missing reader

Example: Catching Errors

import { RequestBatcher } from "@juspay/neurolink";

const batcher = new RequestBatcher({ maxBatchSize: 10, maxWaitMs: 50 });

try {
// Throws missingConfiguration because no executor is set
await batcher.add("myTool", { id: 1 });
} catch (error) {
// error.message: "Missing configuration: batchExecutor"
// error.hint: "Call setExecutor() before adding requests to the batcher"
console.error(error.message);
}

SDK Integration

The MCP enhancement modules can be configured declaratively through the NeuroLink constructor. When provided, these modules are automatically wired into the generate() and stream() execution paths -- no additional setup required.

Constructor Configuration

import { NeuroLink } from "@juspay/neurolink";

const neurolink = new NeuroLink({
mcp: {
cache: { enabled: true, ttl: 60000, maxSize: 200, strategy: "lru" },
annotations: { enabled: true, autoInfer: true },
router: { enabled: true, strategy: "least-loaded", enableAffinity: false },
batcher: { enabled: true, maxBatchSize: 10, maxWaitMs: 100 },
discovery: { enabled: true },
middleware: [loggingMiddleware, confirmationMiddleware],
},
});

How Enhancements Apply to generate()/stream()

When MCP enhancements are configured, ToolsManager (the internal component that wires tools for generate() and stream()) routes every tool call through executeTool(). This means the full enhancement pipeline -- annotation inference, middleware chain, cache lookup, routing, and batching -- applies automatically to every tool invocation during generation and streaming, with no per-call setup required.

MCPEnhancementsConfig Options

FieldTypeDefaultDescription
cache.enabledbooleanfalseEnable tool result caching for read-only tools
cache.ttlnumber300000Cache TTL in milliseconds (5 minutes)
cache.maxSizenumber500Maximum cache entries before eviction
cache.strategyCacheStrategy'lru'Eviction strategy: 'lru', 'fifo', or 'lfu'
annotations.enabledbooleantrueEnable tool annotation auto-inference
annotations.autoInferbooleantrueAuto-infer annotations from tool name and description
router.enabledbooleanautoEnable tool routing. Auto-activates when 2+ external servers exist
router.strategyRoutingStrategy'least-loaded'Routing strategy: 'round-robin', 'least-loaded', 'capability-based', 'affinity', 'priority', 'random'
router.enableAffinitybooleanfalseEnable session affinity (sticky routing)
batcher.enabledbooleanfalseEnable request batching for programmatic executeTool() calls
batcher.maxBatchSizenumber10Maximum requests per batch
batcher.maxWaitMsnumber100Maximum wait time before flushing a batch (ms)
discovery.enabledbooleantrueEnable enhanced tool discovery and search
middlewareToolMiddleware[][]Global middleware chain applied to every tool execution

Per-Request Cache Bypass

You can disable tool caching for individual requests using the disableToolCache option:

const result = await neurolink.generate({
prompt: "What is the current server status?",
disableToolCache: true, // bypass cache for this request
});

const stream = await neurolink.stream({
prompt: "Get the latest metrics",
disableToolCache: true, // also supported in stream()
});

This is useful when you need fresh results for a specific call without disabling caching globally.


The NeuroLink class exposes 15 MCP enhancement methods for programmatic access to routing, caching, batching, annotations, elicitation, discovery, tool conversion, and agent/workflow exposure.

Method Reference

MethodReturn TypeDescription
useToolMiddleware(middleware)thisRegister a global tool middleware; returns this for chaining
getToolMiddlewares()ToolMiddleware[]Get all registered tool middlewares
flushToolBatch()Promise<void>Flush any pending batched tool calls immediately
getMCPEnhancementsConfig()MCPEnhancementsConfig | undefinedGet the current MCP enhancements configuration
getElicitationManager()Promise<ElicitationManager>Get the global elicitation manager for interactive tool input
registerElicitationHandler(handler)Promise<void>Register a handler for interactive elicitation requests
getMultiServerManager()Promise<MultiServerManager>Get the multi-server manager for load balancing and failover
getEnhancedToolDiscovery()Promise<EnhancedToolDiscovery>Get the enhanced tool discovery service
getMCPRegistryClient()Promise<MCPRegistryClient>Get the MCP registry client for discovering servers
exposeAgentAsTool(agent, options?)Promise<ExposureResult>Expose a NeuroLink agent as an MCP tool
exposeWorkflowAsTool(workflow, options?)Promise<ExposureResult>Expose a workflow as an MCP tool
getToolIntegrationManager()Promise<ToolIntegrationManager>Get the tool integration manager for middleware and elicitation
convertToolsToMCPFormat(tools, options?)Promise<MCPTool[]>Convert NeuroLink tools to MCP format
convertToolsFromMCPFormat(tools, options?)Promise<NeuroLinkTool[]>Convert MCP tools to NeuroLink format
getToolAnnotations(toolName)Promise<{ annotations, summary } | null>Get annotations and safety information for a tool

Middleware Chaining

useToolMiddleware returns this, enabling a fluent chaining pattern:

import { NeuroLink } from "@juspay/neurolink";
import type { ToolMiddleware } from "@juspay/neurolink";

const loggingMiddleware: ToolMiddleware = async (
tool,
params,
context,
next,
) => {
console.log(`Calling tool: ${tool.name}`, params);
const result = await next();
console.log(`Tool result: ${tool.name}`, result);
return result;
};

const confirmationMiddleware: ToolMiddleware = async (
tool,
params,
context,
next,
) => {
if (tool.annotations?.destructiveHint) {
const confirmed = await askUser(`Allow ${tool.name}?`);
if (!confirmed) throw new Error("User declined");
}
return next();
};

const neurolink = new NeuroLink();

neurolink
.useToolMiddleware(loggingMiddleware)
.useToolMiddleware(confirmationMiddleware);

Elicitation (Interactive Tool Input)

Use getElicitationManager() or the shorthand registerElicitationHandler() to handle interactive input requests from tools during execution:

// Shorthand: register a handler directly
await neurolink.registerElicitationHandler(async (request) => {
switch (request.type) {
case "confirmation":
return { confirmed: await confirmWithUser(request.message) };
case "text":
return { value: await promptUser(request.message) };
case "select":
return { value: await selectFromOptions(request.options) };
}
});

// Full access: get the manager for advanced configuration
const elicitationManager = await neurolink.getElicitationManager();
elicitationManager.registerHandler(async (request) => {
if (request.type === "confirmation") {
const answer = await askUser(request.message);
return { confirmed: answer === "yes" };
}
});

Agent & Workflow Exposure

Expose agents and workflows as MCP tools so they can be invoked by other systems via the MCP protocol:

// Expose an agent
const agent = {
id: "my-agent",
name: "My Agent",
description: "An agent that processes data",
execute: async (params) => {
/* ... */
},
};
const agentTool = await neurolink.exposeAgentAsTool(agent, {
prefix: "agent_",
});

// Expose a workflow
const workflow = {
id: "data-pipeline",
name: "Data Pipeline",
description: "Runs the data processing pipeline",
execute: async (params) => {
/* ... */
},
steps: [
{ id: "step1", name: "Extract" },
{ id: "step2", name: "Transform" },
{ id: "step3", name: "Load" },
],
};
const workflowTool = await neurolink.exposeWorkflowAsTool(workflow, {
prefix: "workflow_",
includeMetadataInDescription: true,
executionTimeout: 60000,
enableLogging: true,
});

Tool Annotations

Retrieve annotations and safety metadata for any registered tool:

const annotations = await neurolink.getToolAnnotations("deleteFile");
if (annotations) {
console.log(annotations.annotations);
// { destructiveHint: true, idempotentHint: false, readOnlyHint: false, ... }

console.log(annotations.summary);
// Human-readable summary of the tool's safety characteristics
}

Annotations are inferred from the tool name and description. Explicit annotations set on the tool take precedence over inferred values.

Tool Format Conversion

Convert between NeuroLink and MCP tool formats for interoperability:

// Export local tools to MCP format
const mcpTools = await neurolink.convertToolsToMCPFormat(
[{ name: "myTool", description: "Does something", execute: async () => {} }],
{ namespacePrefix: "myapp_" },
);

// Import external MCP tools to NeuroLink format
const neurolinkTools = await neurolink.convertToolsFromMCPFormat(
externalTools,
{
removeNamespacePrefix: "external_",
},
);

CLI Commands

The neurolink mcp command group provides 12 subcommands for managing MCP servers, tools, and annotations from the terminal.

List all configured MCP servers.

neurolink mcp list

Show detailed server status including health and connection info.

neurolink mcp servers
neurolink mcp servers --status connected
neurolink mcp servers --category database
neurolink mcp servers --detailed

List tools across all servers with filtering and search.

neurolink mcp tools
neurolink mcp tools --server github
neurolink mcp tools --category file-system
neurolink mcp tools --tag sql
neurolink mcp tools --safety dangerous
neurolink mcp tools --annotations
neurolink mcp tools --search "read file"

Discover tools from servers with automatic annotation inference.

neurolink mcp discover
neurolink mcp discover --server github
neurolink mcp discover --infer-annotations

Scaffold a new custom MCP server project.

neurolink mcp create-server my-data-server
neurolink mcp create-server my-server --template basic --output ./servers
neurolink mcp create-server my-server --tools readData,writeData,listItems

Add, update, or infer annotations on MCP tools.

neurolink mcp annotate --tool deleteUser --destructive
neurolink mcp annotate --tool getUser --read-only --idempotent
neurolink mcp annotate --infer
neurolink mcp annotate --list

Install a well-known MCP server from the built-in registry.

neurolink mcp install postgres
neurolink mcp install github
neurolink mcp install brave-search

Add a custom MCP server by name and command.

neurolink mcp add my-server "npx -y @my-org/my-mcp-server"
neurolink mcp add data-api "node ./servers/data.js"

Test connectivity to MCP servers.

neurolink mcp test
neurolink mcp test github
neurolink mcp test postgres --timeout 10000

Execute a specific tool on a server with parameters.

neurolink mcp exec github list_repos
neurolink mcp exec postgres query --params '{"sql": "SELECT * FROM users LIMIT 5"}'

Remove a configured MCP server.

neurolink mcp remove my-server

Browse and search the MCP server registry.

neurolink mcp registry search database
neurolink mcp registry list
neurolink mcp registry info postgres
neurolink mcp registry categories
neurolink mcp registry popular

API Reference

Classes

ClassDescription
ToolRouterIntelligent routing across MCP servers
ToolCache<T>Generic cache with LRU/FIFO/LFU eviction
ToolResultCacheTool-specific cache with auto key generation
RequestBatcher<T>Automatic request batching with server grouping
ToolCallBatcherHigh-level batcher for MCP tool calls
ToolIntegrationManagerMiddleware chain manager with elicitation support
EnhancedToolDiscoveryAdvanced tool search and discovery
ElicitationManagerInteractive user input during tool execution
ElicitationProtocolAdapterJSON-RPC protocol bridge for elicitation
MultiServerManagerLoad balancing and failover coordinator
MCPServerBaseAbstract base class for custom MCP servers
AgentExposureManagerLifecycle manager for exposed agents/workflows
ServerCapabilitiesManagerResource and prompt manager per MCP spec
MCPRegistryClientServer discovery from registries

Factory Functions

FunctionReturns
createToolRouter()ToolRouter
createToolCache()ToolCache<T>
createToolResultCache()ToolResultCache
createRequestBatcher()RequestBatcher<T>
createToolCallBatcher()ToolCallBatcher
createAnnotatedTool()MCPServerTool
createToolFromFunction()MCPServerTool
createTextResource()RegisteredResource
createJsonResource()RegisteredResource
createPrompt()RegisteredPrompt

Global Instances

InstanceType
globalMultiServerManagerMultiServerManager
globalElicitationManagerElicitationManager
globalElicitationProtocolElicitationProtocolAdapter
globalToolIntegrationManagerToolIntegrationManager
globalAgentExposureManagerAgentExposureManager
globalMCPRegistryClientMCPRegistryClient

Utility Functions

FunctionDescription
inferAnnotations()Infer annotations from tool name/description
mergeAnnotations()Merge annotation objects with precedence
validateAnnotations()Validate annotations for conflicts
getToolSafetyLevel()Get safety level: safe, moderate, dangerous
requiresConfirmation()Check if tool needs user confirmation
isSafeToRetry()Check if tool is safe for automatic retry
filterToolsByAnnotations()Filter tools by annotation predicate
getAnnotationSummary()Get human-readable annotation summary
neuroLinkToolToMCP()Convert NeuroLink tool to MCP format
mcpToolToNeuroLink()Convert MCP tool to NeuroLink format
batchConvertToMCP()Batch convert tools to MCP format
batchConvertToNeuroLink()Batch convert tools to NeuroLink format
validateToolName()Validate tool name per MCP spec
sanitizeToolName()Sanitize tool name for MCP compatibility
getWellKnownServer()Look up a well-known MCP server by ID
getAllWellKnownServers()Get all well-known MCP servers
exposeAgentAsTool()Expose an agent as an MCP tool
exposeWorkflowAsTool()Expose a workflow as an MCP tool
exposeAgentsAsTools()Batch expose agents
exposeWorkflowsAsTools()Batch expose workflows
isElicitationProtocolMessage()Check if message is elicitation protocol
createConfirmationRequest()Create protocol confirmation request
createTextInputRequest()Create protocol text input request
createSelectRequest()Create protocol select request
createFormRequest()Create protocol form request

Testing

The MCP enhancements include a comprehensive continuous test suite covering all 14 modules.

Running the Test Suite

npx tsx test/continuous-test-suite-mcp.ts --provider=vertex

Environment Variables

VariableDescriptionDefault
TEST_PROVIDERAI provider to use for integration testsvertex
TEST_MODELModel name overrideProvider default model

Test Coverage

The suite contains 44 test functions with 172+ assertions organized across 9 parts:

PartModuleTestsFocus
1Tool Router5Strategies, registration, affinity, health, events
2Tool Cache5Set/get, TTL, eviction, invalidation, stats
3Request Batcher5Batching, flush, drain, server grouping, events
4Tool Annotations5Inference, safety levels, validation, merge, filter
5Tool Converter5Bidirectional conversion, batch, function, sanitize
6Tool Integration & Middleware5Middleware chain, elicitation, timeout, retry, custom
7Enhanced Discovery & Multi-Server5Search, safety filter, server groups, namespacing
8Server Base, Exposure, Registry5Custom server, agent/workflow exposure, registry
9Server Capabilities & Elicitation4Resources, prompts, subscriptions, elicitation types