@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
503 lines (502 loc) • 15.8 kB
JavaScript
/**
* MCP Server Capabilities - Resources and Prompts
*
* Extends MCP server functionality with resource and prompt handling
* according to MCP specification. This module provides:
* - Resource registration and management
* - Prompt template registration and execution
* - Resource subscription support
*
* @module mcp/serverCapabilities
* @since 8.39.0
*/
import { EventEmitter } from "events";
import { logger } from "../utils/logger.js";
import { ErrorFactory } from "../utils/errorHandling.js";
import { withTimeout } from "../utils/async/withTimeout.js";
/**
* Server Capabilities Manager
*
* Manages resources and prompts for MCP servers.
*
* @example
* ```typescript
* const capabilities = new ServerCapabilitiesManager({
* resources: true,
* prompts: true,
* });
*
* // Register a resource
* capabilities.registerResource({
* uri: "file:///data/config.json",
* name: "Configuration",
* mimeType: "application/json",
* reader: async (uri) => ({
* uri,
* mimeType: "application/json",
* text: JSON.stringify({ key: "value" }),
* }),
* });
*
* // Register a prompt
* capabilities.registerPrompt({
* name: "summarize",
* description: "Summarize text content",
* arguments: [{ name: "text", required: true }],
* generator: async (args) => ({
* messages: [
* { role: "user", content: { type: "text", text: `Summarize: ${args.text}` } },
* ],
* }),
* });
* ```
*/
export class ServerCapabilitiesManager extends EventEmitter {
config;
resources = new Map();
prompts = new Map();
subscriptions = new Map();
resourceTemplates = new Map();
constructor(config = {}) {
super();
this.config = {
resources: config.resources ?? true,
prompts: config.prompts ?? true,
resourceSubscriptions: config.resourceSubscriptions ?? true,
};
}
// ========================================
// RESOURCE MANAGEMENT
// ========================================
/**
* Register a resource
*/
registerResource(resource) {
if (!this.config.resources) {
throw ErrorFactory.invalidConfiguration("resources", "Resource support is not enabled");
}
this.validateResourceUri(resource.uri);
this.resources.set(resource.uri, resource);
this.emit("resourceRegistered", {
uri: resource.uri,
name: resource.name,
timestamp: new Date(),
});
logger.debug(`[ServerCapabilities] Registered resource: ${resource.uri}`);
return this;
}
/**
* Register a resource template (with URI pattern)
*/
registerResourceTemplate(pattern, template) {
if (!this.config.resources) {
throw ErrorFactory.invalidConfiguration("resources", "Resource support is not enabled");
}
this.resourceTemplates.set(pattern, {
...template,
uri: template.uriPattern,
});
this.emit("resourceTemplateRegistered", {
pattern,
timestamp: new Date(),
});
return this;
}
/**
* Unregister a resource
*/
unregisterResource(uri) {
const removed = this.resources.delete(uri);
if (removed) {
// Clear subscriptions
this.subscriptions.delete(uri);
this.emit("resourceUnregistered", {
uri,
timestamp: new Date(),
});
}
return removed;
}
/**
* List all resources
*/
listResources() {
return Array.from(this.resources.values()).map((r) => ({
uri: r.uri,
name: r.name,
description: r.description,
mimeType: r.mimeType,
size: r.size,
dynamic: r.dynamic,
annotations: r.annotations,
}));
}
/**
* Read a resource
*/
async readResource(uri, context) {
// Check direct resources
let resource = this.resources.get(uri);
// Check templates if not found
if (!resource) {
resource = this.findResourceTemplate(uri);
}
if (!resource) {
throw ErrorFactory.invalidConfiguration("resource", `Resource not found: ${uri}`);
}
const startTime = Date.now();
const resourceTimeoutMs = 30_000;
try {
const content = await withTimeout(resource.reader(uri, context), resourceTimeoutMs, `Resource read timed out after ${resourceTimeoutMs}ms for URI: ${uri}`);
const duration = Date.now() - startTime;
this.emit("resourceRead", {
uri,
duration,
success: true,
timestamp: new Date(),
});
return content;
}
catch (error) {
const duration = Date.now() - startTime;
this.emit("resourceRead", {
uri,
duration,
success: false,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date(),
});
throw error;
}
}
/**
* Subscribe to resource changes
*/
subscribeToResource(uri, callback) {
if (!this.config.resourceSubscriptions) {
throw ErrorFactory.invalidConfiguration("resourceSubscriptions", "Resource subscriptions are not enabled");
}
if (!this.subscriptions.has(uri)) {
this.subscriptions.set(uri, new Set());
}
const subs = this.subscriptions.get(uri);
if (subs) {
subs.add(callback);
}
this.emit("resourceSubscribed", {
uri,
timestamp: new Date(),
});
// Return unsubscribe function
return () => {
const subs = this.subscriptions.get(uri);
if (subs) {
subs.delete(callback);
if (subs.size === 0) {
this.subscriptions.delete(uri);
}
}
this.emit("resourceUnsubscribed", {
uri,
timestamp: new Date(),
});
};
}
/**
* Notify subscribers of resource change
*/
async notifyResourceChanged(uri) {
const subscribers = this.subscriptions.get(uri);
if (!subscribers || subscribers.size === 0) {
return;
}
try {
const content = await this.readResource(uri);
for (const callback of subscribers) {
try {
const result = callback(uri, content);
// Handle async callbacks that return promises
if (result && typeof result.catch === "function") {
result.catch((error) => {
logger.error(`[ServerCapabilities] Async error notifying subscriber for ${uri}:`, error);
});
}
}
catch (error) {
logger.error(`[ServerCapabilities] Error notifying subscriber for ${uri}:`, error);
}
}
this.emit("resourceChanged", {
uri,
subscriberCount: subscribers.size,
timestamp: new Date(),
});
}
catch (error) {
logger.error(`[ServerCapabilities] Error reading resource for notification: ${uri}`, error);
}
}
/**
* Get resource by URI
*/
getResource(uri) {
return this.resources.get(uri) ?? this.findResourceTemplate(uri);
}
/**
* Validate resource URI
*/
validateResourceUri(uri) {
try {
new URL(uri);
}
catch {
// Allow non-URL URIs but warn
logger.warn(`[ServerCapabilities] Resource URI is not a valid URL: ${uri}`);
}
}
/**
* Find matching resource template
*/
findResourceTemplate(uri) {
for (const [pattern, template] of this.resourceTemplates) {
if (this.matchesPattern(uri, pattern)) {
return {
...template,
uri,
};
}
}
return undefined;
}
/**
* Check if URI matches a pattern
*/
matchesPattern(uri, pattern) {
// Guard against excessively long patterns that could cause ReDoS
if (pattern.length > 200) {
return false;
}
// Escape regex metacharacters, then replace glob wildcards
const regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".")
// Restore URI template placeholders: \{...\} -> [^/]+
.replace(/\\\{[^}]*\\\}/g, "[^/]+");
try {
return new RegExp(`^${regexPattern}$`).test(uri);
}
catch {
// Invalid regex pattern — treat as non-match
return false;
}
}
// ========================================
// PROMPT MANAGEMENT
// ========================================
/**
* Register a prompt
*/
registerPrompt(prompt) {
if (!this.config.prompts) {
throw ErrorFactory.invalidConfiguration("prompts", "Prompt support is not enabled");
}
this.validatePromptName(prompt.name);
this.prompts.set(prompt.name, prompt);
this.emit("promptRegistered", {
name: prompt.name,
timestamp: new Date(),
});
logger.debug(`[ServerCapabilities] Registered prompt: ${prompt.name}`);
return this;
}
/**
* Unregister a prompt
*/
unregisterPrompt(name) {
const removed = this.prompts.delete(name);
if (removed) {
this.emit("promptUnregistered", {
name,
timestamp: new Date(),
});
}
return removed;
}
/**
* List all prompts
*/
listPrompts() {
return Array.from(this.prompts.values()).map((p) => ({
name: p.name,
description: p.description,
arguments: p.arguments,
}));
}
/**
* Get a prompt
*/
async getPrompt(name, args = {}, context) {
const prompt = this.prompts.get(name);
if (!prompt) {
throw ErrorFactory.invalidConfiguration("prompt", `Prompt not found: ${name}`);
}
// Validate required arguments
for (const arg of prompt.arguments ?? []) {
if (arg.required && args[arg.name] === undefined) {
throw ErrorFactory.invalidConfiguration("promptArgument", `Missing required argument: ${arg.name}`);
}
}
const startTime = Date.now();
const promptTimeoutMs = 30_000;
try {
const result = await withTimeout(prompt.generator(args, context), promptTimeoutMs, `Prompt generation timed out after ${promptTimeoutMs}ms for prompt: ${name}`);
const duration = Date.now() - startTime;
this.emit("promptGenerated", {
name,
duration,
success: true,
messageCount: result.messages.length,
timestamp: new Date(),
});
return result;
}
catch (error) {
const duration = Date.now() - startTime;
this.emit("promptGenerated", {
name,
duration,
success: false,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date(),
});
throw error;
}
}
/**
* Get prompt by name
*/
getPromptDefinition(name) {
return this.prompts.get(name);
}
/**
* Validate prompt name
*/
validatePromptName(name) {
if (!name || typeof name !== "string") {
throw ErrorFactory.invalidConfiguration("promptName", "Prompt name is required and must be a string");
}
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name)) {
throw ErrorFactory.invalidConfiguration("promptName", "Prompt name must start with a letter or underscore and contain only alphanumeric characters, underscores, and hyphens");
}
if (this.prompts.has(name)) {
throw ErrorFactory.invalidConfiguration("promptName", `Prompt '${name}' is already registered`);
}
}
// ========================================
// UTILITY METHODS
// ========================================
/**
* Get capabilities object for MCP protocol
*/
getCapabilities() {
const capabilities = {};
if (this.config.resources) {
capabilities.resources = {
subscribe: this.config.resourceSubscriptions,
listChanged: true,
};
}
if (this.config.prompts) {
capabilities.prompts = {
listChanged: true,
};
}
return capabilities;
}
/**
* Get statistics
*/
getStatistics() {
let subscriptionCount = 0;
for (const subs of this.subscriptions.values()) {
subscriptionCount += subs.size;
}
return {
resourceCount: this.resources.size,
templateCount: this.resourceTemplates.size,
promptCount: this.prompts.size,
subscriptionCount,
};
}
/**
* Clear all resources and prompts
*/
clear() {
this.resources.clear();
this.resourceTemplates.clear();
this.prompts.clear();
this.subscriptions.clear();
this.emit("cleared", { timestamp: new Date() });
}
}
/**
* Create a simple text resource
*/
export function createTextResource(uri, name, content, options) {
return {
uri,
name,
description: options?.description,
mimeType: "text/plain",
dynamic: options?.dynamic ?? typeof content === "function",
reader: async (requestUri) => ({
uri: requestUri,
mimeType: "text/plain",
text: typeof content === "function" ? await content() : content,
}),
};
}
/**
* Create a JSON resource
*/
export function createJsonResource(uri, name, content, options) {
return {
uri,
name,
description: options?.description,
mimeType: "application/json",
dynamic: options?.dynamic ?? typeof content === "function",
reader: async (requestUri) => ({
uri: requestUri,
mimeType: "application/json",
text: JSON.stringify(typeof content === "function" ? await content() : content, null, 2),
}),
};
}
/**
* Create a simple prompt template
*/
export function createPrompt(name, template, options) {
return {
name,
description: options?.description,
arguments: options?.arguments,
generator: async (args) => {
// Simple template substitution
let text = template;
for (const [key, value] of Object.entries(args)) {
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
text = text.replace(new RegExp(`\\{${escapedKey}\\}`, "g"), () => String(value));
}
return {
messages: [
{
role: "user",
content: { type: "text", text },
},
],
};
},
};
}