diff --git a/.eslintrc.js b/.eslintrc.js index cbb34c74f..539c64265 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,10 @@ module.exports = { 'client/dist/**/*', 'client/public/**/*', 'e2e/playwright-report/**/*', + 'packages/mcp/types/**/*', + 'packages/mcp/dist/**/*', + 'packages/mcp/test_bundle/**/*', + 'api/demo/**/*', 'packages/data-provider/types/**/*', 'packages/data-provider/dist/**/*', 'packages/data-provider/test_bundle/**/*', @@ -136,6 +140,30 @@ module.exports = { }, ], }, + { + files: './api/demo/**/*.ts', + overrides: [ + { + files: '**/*.ts', + parser: '@typescript-eslint/parser', + parserOptions: { + project: './packages/data-provider/tsconfig.json', + }, + }, + ], + }, + { + files: './packages/mcp/**/*.ts', + overrides: [ + { + files: '**/*.ts', + parser: '@typescript-eslint/parser', + parserOptions: { + project: './packages/mcp/tsconfig.json', + }, + }, + ], + }, { files: './config/translations/**/*.ts', parser: '@typescript-eslint/parser', @@ -149,6 +177,18 @@ module.exports = { project: './packages/data-provider/tsconfig.spec.json', }, }, + { + files: ['./api/demo/specs/**/*.ts'], + parserOptions: { + project: './packages/data-provider/tsconfig.spec.json', + }, + }, + { + files: ['./packages/mcp/specs/**/*.ts'], + parserOptions: { + project: './packages/mcp/tsconfig.spec.json', + }, + }, ], settings: { react: { diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml index 52560009a..33316731a 100644 --- a/.github/workflows/backend-review.yml +++ b/.github/workflows/backend-review.yml @@ -33,8 +33,11 @@ jobs: - name: Install dependencies run: npm ci - - name: Install Data Provider + - name: Install Data Provider Package run: npm run build:data-provider + + - name: Install MCP Package + run: npm run build:mcp - name: Create empty auth.json file run: | diff --git a/Dockerfile.multi b/Dockerfile.multi index 4d58de0c8..417c3678a 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -10,6 +10,7 @@ RUN npm config set fetch-retry-maxtimeout 600000 && \ npm config set fetch-retry-mintimeout 15000 COPY package*.json ./ COPY packages/data-provider/package*.json ./packages/data-provider/ +COPY packages/mcp/package*.json ./packages/mcp/ COPY client/package*.json ./client/ COPY api/package*.json ./api/ RUN npm ci @@ -21,6 +22,14 @@ COPY packages/data-provider ./ RUN npm run build RUN npm prune --production +# Build mcp package +FROM base AS mcp-build +WORKDIR /app/packages/mcp +COPY packages/mcp ./ +COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/data-provider/dist +RUN npm run build +RUN npm prune --production + # Client build FROM base AS client-build WORKDIR /app/client @@ -36,9 +45,10 @@ WORKDIR /app COPY api ./api COPY config ./config COPY --from=data-provider-build /app/packages/data-provider/dist ./packages/data-provider/dist +COPY --from=mcp-build /app/packages/mcp/dist ./packages/mcp/dist COPY --from=client-build /app/client/dist ./client/dist WORKDIR /app/api RUN npm prune --production EXPOSE 3080 ENV HOST=0.0.0.0 -CMD ["node", "server/index.js"] +CMD ["node", "server/index.js"] \ No newline at end of file diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 401bef4f5..a8d0ec13f 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -1,4 +1,4 @@ -const { Tools } = require('librechat-data-provider'); +const { Tools, Constants } = require('librechat-data-provider'); const { SerpAPI } = require('@langchain/community/tools/serpapi'); const { Calculator } = require('@langchain/community/tools/calculator'); const { createCodeExecutionTool, EnvVar } = require('@librechat/agents'); @@ -17,9 +17,12 @@ const { } = require('../'); const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch'); +const { createMCPTool } = require('~/server/services/MCP'); const { loadSpecs } = require('./loadSpecs'); const { logger } = require('~/config'); +const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`); + /** * Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values. * Tools without required authentication or with valid authentication are considered valid. @@ -142,10 +145,25 @@ const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => }; }; +/** + * + * @param {object} object + * @param {string} object.user + * @param {Agent} [object.agent] + * @param {string} [object.model] + * @param {EModelEndpoint} [object.endpoint] + * @param {LoadToolOptions} [object.options] + * @param {boolean} [object.useSpecs] + * @param {Array} object.tools + * @param {boolean} [object.functions] + * @param {boolean} [object.returnMap] + * @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object } | Record>} + */ const loadTools = async ({ user, + agent, model, - isAgent, + endpoint, useSpecs, tools = [], options = {}, @@ -182,8 +200,9 @@ const loadTools = async ({ toolConstructors.dalle = DALLE3; } + /** @type {ImageGenOptions} */ const imageGenOptions = { - isAgent, + isAgent: !!agent, req: options.req, fileStrategy: options.fileStrategy, processFileURL: options.processFileURL, @@ -240,6 +259,15 @@ const loadTools = async ({ return createFileSearchTool({ req: options.req, files }); }; continue; + } else if (mcpToolPattern.test(tool)) { + requestedTools[tool] = async () => + createMCPTool({ + req: options.req, + toolKey: tool, + model: agent?.model ?? model, + provider: agent?.provider ?? endpoint, + }); + continue; } if (customConstructors[tool]) { diff --git a/api/config/index.js b/api/config/index.js index 3198ff2fb..c66d92ae4 100644 --- a/api/config/index.js +++ b/api/config/index.js @@ -1,5 +1,22 @@ +const { EventSource } = require('eventsource'); const logger = require('./winston'); +global.EventSource = EventSource; + +let mcpManager = null; + +/** + * @returns {Promise} + */ +async function getMCPManager() { + if (!mcpManager) { + const { MCPManager } = await import('librechat-mcp'); + mcpManager = MCPManager.getInstance(logger); + } + return mcpManager; +} + module.exports = { logger, + getMCPManager, }; diff --git a/api/package.json b/api/package.json index 1b57beb79..dc14a6db7 100644 --- a/api/package.json +++ b/api/package.json @@ -44,7 +44,7 @@ "@langchain/google-genai": "^0.1.4", "@langchain/google-vertexai": "^0.1.2", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^1.8.5", + "@librechat/agents": "^1.8.8", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", @@ -73,6 +73,7 @@ "klona": "^2.0.6", "langchain": "^0.2.19", "librechat-data-provider": "*", + "librechat-mcp": "*", "lodash": "^4.17.21", "meilisearch": "^0.38.0", "mime": "^3.0.0", diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 3c7085c2a..2cdbd1549 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -1,6 +1,8 @@ const { promises: fs } = require('fs'); const { CacheKeys, AuthType } = require('librechat-data-provider'); const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs'); +const { getCustomConfig } = require('~/server/services/Config'); +const { getMCPManager } = require('~/config'); const { getLogStores } = require('~/cache'); /** @@ -107,6 +109,12 @@ const getAvailableTools = async (req, res) => { const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8'); const jsonData = JSON.parse(pluginManifest); + const customConfig = await getCustomConfig(); + if (customConfig?.mcpServers != null) { + const mcpManager = await getMCPManager(); + await mcpManager.loadManifestTools(jsonData); + } + /** @type {TPlugin[]} */ const uniquePlugins = filterUniquePlugins(jsonData); diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 08fceeb3c..e0bb52d26 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1,4 +1,4 @@ -const { Tools, StepTypes, imageGenTools } = require('librechat-data-provider'); +const { Tools, StepTypes, imageGenTools, FileContext } = require('librechat-data-provider'); const { EnvVar, GraphEvents, @@ -6,6 +6,7 @@ const { ChatModelStreamHandler, } = require('@librechat/agents'); const { processCodeOutput } = require('~/server/services/Files/Code/process'); +const { saveBase64Image } = require('~/server/services/Files/process'); const { loadAuthValues } = require('~/app/clients/tools/util'); const { logger } = require('~/config'); @@ -191,7 +192,11 @@ function createToolEndCallback({ req, res, artifactPromises }) { return; } - if (imageGenTools.has(output.name) && output.artifact) { + if (!output.artifact) { + return; + } + + if (imageGenTools.has(output.name)) { artifactPromises.push( (async () => { const fileMetadata = Object.assign(output.artifact, { @@ -217,10 +222,53 @@ function createToolEndCallback({ req, res, artifactPromises }) { return; } - if (output.name !== Tools.execute_code) { + if (output.artifact.content) { + /** @type {FormattedContent[]} */ + const content = output.artifact.content; + for (const part of content) { + if (part.type !== 'image_url') { + continue; + } + const { url } = part.image_url; + artifactPromises.push( + (async () => { + const filename = `${output.tool_call_id}-image-${new Date().getTime()}`; + const file = await saveBase64Image(url, { + req, + filename, + endpoint: metadata.provider, + context: FileContext.image_generation, + }); + const fileMetadata = Object.assign(file, { + messageId: metadata.run_id, + toolCallId: output.tool_call_id, + conversationId: metadata.thread_id, + }); + if (!res.headersSent) { + return fileMetadata; + } + + if (!fileMetadata) { + return null; + } + + res.write(`event: attachment\ndata: ${JSON.stringify(fileMetadata)}\n\n`); + return fileMetadata; + })().catch((error) => { + logger.error('Error processing artifact content:', error); + return null; + }), + ); + } return; } + { + if (output.name !== Tools.execute_code) { + return; + } + } + if (!output.artifact.files) { return; } diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 0ec27962a..b4ca874ae 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -9,6 +9,7 @@ const { azureConfigSetup } = require('./start/azureOpenAI'); const { loadAndFormatTools } = require('./ToolService'); const { agentsConfigSetup } = require('./start/agents'); const { initializeRoles } = require('~/models/Role'); +const { getMCPManager } = require('~/config'); const paths = require('~/config/paths'); /** @@ -39,11 +40,17 @@ const AppService = async (app) => { /** @type {Record { const { tools, toolContextMap } = await loadAgentTools({ req, - tools: agent.tools, - agent_id: agent.id, + agent, tool_resources, }); diff --git a/api/server/services/Files/images/resize.js b/api/server/services/Files/images/resize.js index 531c9a2c6..50bec1ef3 100644 --- a/api/server/services/Files/images/resize.js +++ b/api/server/services/Files/images/resize.js @@ -58,7 +58,12 @@ async function resizeImageBuffer(inputBuffer, resolution, endpoint) { const resizedBuffer = await sharp(inputBuffer).rotate().resize(resizeOptions).toBuffer(); const resizedMetadata = await sharp(resizedBuffer).metadata(); - return { buffer: resizedBuffer, width: resizedMetadata.width, height: resizedMetadata.height }; + return { + buffer: resizedBuffer, + bytes: resizedMetadata.size, + width: resizedMetadata.width, + height: resizedMetadata.height, + }; } /** diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index ab401420f..198dc9400 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -18,8 +18,12 @@ const { isAssistantsEndpoint, } = require('librechat-data-provider'); const { EnvVar } = require('@librechat/agents'); +const { + convertImage, + resizeAndConvert, + resizeImageBuffer, +} = require('~/server/services/Files/images'); const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2'); -const { convertImage, resizeAndConvert } = require('~/server/services/Files/images'); const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); @@ -736,6 +740,73 @@ async function retrieveAndProcessFile({ } } +/** + * Converts a base64 string to a buffer. + * @param {string} base64String + * @returns {Buffer} + */ +function base64ToBuffer(base64String) { + try { + const typeMatch = base64String.match(/^data:([A-Za-z-+/]+);base64,/); + const type = typeMatch ? typeMatch[1] : ''; + + const base64Data = base64String.replace(/^data:([A-Za-z-+/]+);base64,/, ''); + + if (!base64Data) { + throw new Error('Invalid base64 string'); + } + + return { + buffer: Buffer.from(base64Data, 'base64'), + type, + }; + } catch (error) { + throw new Error(`Failed to convert base64 to buffer: ${error.message}`); + } +} + +async function saveBase64Image( + url, + { req, file_id: _file_id, filename: _filename, endpoint, context, resolution = 'high' }, +) { + const file_id = _file_id ?? v4(); + + let filename = _filename; + const { buffer: inputBuffer, type } = base64ToBuffer(url); + if (!path.extname(_filename)) { + const extension = mime.getExtension(type); + if (extension) { + filename += `.${extension}`; + } else { + throw new Error(`Could not determine file extension from MIME type: ${type}`); + } + } + + const image = await resizeImageBuffer(inputBuffer, resolution, endpoint); + const source = req.app.locals.fileStrategy; + const { saveBuffer } = getStrategyFunctions(source); + const filepath = await saveBuffer({ + userId: req.user.id, + fileName: filename, + buffer: image.buffer, + }); + return await createFile( + { + type, + source, + context, + file_id, + filepath, + filename, + user: req.user.id, + bytes: image.bytes, + width: image.width, + height: image.height, + }, + true, + ); +} + /** * Filters a file based on its size and the endpoint origin. * @@ -810,6 +881,7 @@ module.exports = { filterFile, processFiles, processFileURL, + saveBase64Image, processImageFile, uploadImageBuffer, processFileUpload, diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js new file mode 100644 index 000000000..4b23939e6 --- /dev/null +++ b/api/server/services/MCP.js @@ -0,0 +1,57 @@ +const { tool } = require('@langchain/core/tools'); +const { Constants: AgentConstants } = require('@librechat/agents'); +const { + Constants, + convertJsonSchemaToZod, + isAssistantsEndpoint, +} = require('librechat-data-provider'); +const { logger, getMCPManager } = require('~/config'); + +/** + * Creates a general tool for an entire action set. + * + * @param {Object} params - The parameters for loading action sets. + * @param {ServerRequest} params.req - The name of the tool. + * @param {string} params.toolKey - The toolKey for the tool. + * @param {import('@librechat/agents').Providers | EModelEndpoint} params.provider - The provider for the tool. + * @param {string} params.model - The model for the tool. + * @returns { Promise unknown}> } An object with `_call` method to execute the tool input. + */ +async function createMCPTool({ req, toolKey, provider }) { + const toolDefinition = req.app.locals.availableTools[toolKey]?.function; + if (!toolDefinition) { + logger.error(`Tool ${toolKey} not found in available tools`); + return null; + } + /** @type {LCTool} */ + const { description, parameters } = toolDefinition; + const schema = convertJsonSchemaToZod(parameters); + const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter); + /** @type {(toolInput: Object | string) => Promise} */ + const _call = async (toolInput) => { + try { + const mcpManager = await getMCPManager(); + const result = await mcpManager.callTool(serverName, toolName, provider, toolInput); + if (isAssistantsEndpoint(provider) && Array.isArray(result)) { + return result[0]; + } + return result; + } catch (error) { + logger.error(`${toolName} MCP server tool call failed`, error); + return `${toolName} MCP server tool call failed.`; + } + }; + + const toolInstance = tool(_call, { + schema, + name: toolKey, + description: description || '', + responseFormat: AgentConstants.CONTENT_AND_ARTIFACT, + }); + toolInstance.mcp = true; + return toolInstance; +} + +module.exports = { + createMCPTool, +}; diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 521186624..5ac0f768b 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -176,6 +176,7 @@ async function processRequiredActions(client, requiredActions) { model: client.req.body.model ?? 'gpt-4o-mini', tools, functions: true, + endpoint: client.req.body.endpoint, options: { processFileURL, req: client.req, @@ -374,22 +375,19 @@ async function processRequiredActions(client, requiredActions) { * Processes the runtime tool calls and returns the tool classes. * @param {Object} params - Run params containing user and request information. * @param {ServerRequest} params.req - The request object. - * @param {string} params.agent_id - The agent ID. - * @param {Agent['tools']} params.tools - The agent's available tools. - * @param {Agent['tool_resources']} params.tool_resources - The agent's available tool resources. + * @param {Agent} params.agent - The agent to load tools for. * @param {string | undefined} [params.openAIApiKey] - The OpenAI API key. * @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools. */ -async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiKey }) { - if (!tools || tools.length === 0) { +async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) { + if (!agent.tools || agent.tools.length === 0) { return {}; } const { loadedTools, toolContextMap } = await loadTools({ - user: req.user.id, - // model: req.body.model ?? 'gpt-4o-mini', - tools, + agent, functions: true, - isAgent: agent_id != null, + user: req.user.id, + tools: agent.tools, options: { req, openAIApiKey, @@ -409,6 +407,11 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK continue; } + if (tool.mcp === true) { + agentTools.push(tool); + continue; + } + const toolDefinition = { name: tool.name, schema: tool.schema, @@ -434,10 +437,10 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK let actionSets = []; const ActionToolMap = {}; - for (const toolName of tools) { + for (const toolName of agent.tools) { if (!ToolMap[toolName]) { if (!actionSets.length) { - actionSets = (await loadActionSets({ agent_id })) ?? []; + actionSets = (await loadActionSets({ agent_id: agent.id })) ?? []; } let actionSet = null; @@ -473,7 +476,7 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK }); if (!tool) { logger.warn( - `Invalid action: user: ${req.user.id} | agent_id: ${agent_id} | toolName: ${toolName}`, + `Invalid action: user: ${req.user.id} | agent_id: ${agent.id} | toolName: ${toolName}`, ); throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`); } @@ -485,7 +488,7 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK } } - if (tools.length > 0 && agentTools.length === 0) { + if (agent.tools.length > 0 && agentTools.length === 0) { throw new Error('No tools found for the specified tool calls.'); } diff --git a/api/typedefs.js b/api/typedefs.js index 907568f5f..cd6b3ad0c 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -62,6 +62,12 @@ * @memberof typedefs */ +/** + * @exports ConversationSummaryBufferMemory + * @typedef {import('langchain/memory').ConversationSummaryBufferMemory} ConversationSummaryBufferMemory + * @memberof typedefs + */ + /** * @exports UsageMetadata * @typedef {import('@langchain/core/messages').UsageMetadata} UsageMetadata @@ -746,6 +752,33 @@ * @memberof typedefs */ +/** + * + * @typedef {Object} ImageGenOptions + * @property {ServerRequest} req - The request object. + * @property {boolean} isAgent - Whether the request is from an agent. + * @property {FileSources} fileStrategy - The file strategy to use. + * @property {processFileURL} processFileURL - The function to process a file URL. + * @property {boolean} returnMetadata - Whether to return metadata. + * @property {uploadImageBuffer} uploadImageBuffer - The function to upload an image buffer. + * @memberof typedefs + */ + +/** + * @typedef {Partial & { + * message?: string, + * signal?: AbortSignal + * memory?: ConversationSummaryBufferMemory + * }} LoadToolOptions + * @memberof typedefs + */ + +/** + * @exports EModelEndpoint + * @typedef {import('librechat-data-provider').EModelEndpoint} EModelEndpoint + * @memberof typedefs + */ + /** * @exports TAttachment * @typedef {import('librechat-data-provider').TAttachment} TAttachment @@ -866,6 +899,42 @@ * @memberof typedefs */ +/** + * @exports JsonSchemaType + * @typedef {import('librechat-data-provider').JsonSchemaType} JsonSchemaType + * @memberof typedefs + */ + +/** + * @exports MCPServers + * @typedef {import('librechat-mcp').MCPServers} MCPServers + * @memberof typedefs + */ + +/** + * @exports MCPManager + * @typedef {import('librechat-mcp').MCPManager} MCPManager + * @memberof typedefs + */ + +/** + * @exports LCAvailableTools + * @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools + * @memberof typedefs + */ + +/** + * @exports LCTool + * @typedef {import('librechat-mcp').LCTool} LCTool + * @memberof typedefs + */ + +/** + * @exports FormattedContent + * @typedef {import('librechat-mcp').FormattedContent} FormattedContent + * @memberof typedefs + */ + /** * Represents details of the message creation by the run step, including the ID of the created message. * diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index 756fbd487..76e39a21b 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -35,8 +35,30 @@ export default function ToolCall({ const circumference = 2 * Math.PI * radius; const offset = circumference - progress * circumference; - const [function_name, _domain] = name.split(actionDelimiter) as [string, string | undefined]; - const domain = _domain?.replaceAll(actionDomainSeparator, '.') ?? null; + const { function_name, domain, isMCPToolCall } = useMemo(() => { + if (typeof name !== 'string') { + return { function_name: '', domain: null, isMCPToolCall: false }; + } + + if (name.includes(Constants.mcp_delimiter)) { + const [func, server] = name.split(Constants.mcp_delimiter); + return { + function_name: func || '', + domain: server && (server.replaceAll(actionDomainSeparator, '.') || null), + isMCPToolCall: true, + }; + } + + const [func, _domain] = name.includes(actionDelimiter) + ? name.split(actionDelimiter) + : [name, '']; + return { + function_name: func || '', + domain: _domain && (_domain.replaceAll(actionDomainSeparator, '.') || null), + isMCPToolCall: false, + }; + }, [name]); + const error = typeof output === 'string' && output.toLowerCase().includes('error processing tool'); @@ -83,6 +105,9 @@ export default function ToolCall({ }; const getFinishedText = () => { + if (isMCPToolCall === true) { + return localize('com_assistants_completed_function', function_name); + } if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) { return localize('com_assistants_completed_action', domain); } diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 9b6f504b5..2230d1420 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { Check, X } from 'lucide-react'; import { useParams } from 'react-router-dom'; @@ -6,7 +6,7 @@ import { Constants } from 'librechat-data-provider'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; import type { TConversation } from 'librechat-data-provider'; -import { useConversations, useNavigateToConvo, useMediaQuery } from '~/hooks'; +import { useConversations, useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks'; import { useUpdateConversationMutation } from '~/data-provider'; import EndpointIcon from '~/components/Endpoints/EndpointIcon'; import { NotificationSeverity } from '~/common'; @@ -14,7 +14,6 @@ import { useToastContext } from '~/Providers'; import { ConvoOptions } from './ConvoOptions'; import { cn } from '~/utils'; import store from '~/store'; -import { useLocalize } from '~/hooks' type KeyEvent = KeyboardEvent; @@ -71,11 +70,11 @@ export default function Conversation({ ); }; - const renameHandler: (e: MouseEvent) => void = () => { + const renameHandler = useCallback(() => { setIsPopoverActive(false); setTitleInput(title); setRenaming(true); - }; + }, [title]); useEffect(() => { if (renaming && inputRef.current) { @@ -83,64 +82,76 @@ export default function Conversation({ } }, [renaming]); - const onRename = (e: MouseEvent | FocusEvent | KeyEvent) => { - e.preventDefault(); - setRenaming(false); - if (titleInput === title) { - return; - } - if (typeof conversationId !== 'string' || conversationId === '') { - return; - } + const onRename = useCallback( + (e: MouseEvent | FocusEvent | KeyEvent) => { + e.preventDefault(); + setRenaming(false); + if (titleInput === title) { + return; + } + if (typeof conversationId !== 'string' || conversationId === '') { + return; + } - updateConvoMutation.mutate( - { conversationId, title: titleInput ?? '' }, - { - onSuccess: () => refreshConversations(), - onError: () => { - setTitleInput(title); - showToast({ - message: 'Failed to rename conversation', - severity: NotificationSeverity.ERROR, - showIcon: true, - }); + updateConvoMutation.mutate( + { conversationId, title: titleInput ?? '' }, + { + onSuccess: () => refreshConversations(), + onError: () => { + setTitleInput(title); + showToast({ + message: 'Failed to rename conversation', + severity: NotificationSeverity.ERROR, + showIcon: true, + }); + }, }, - }, - ); - }; + ); + }, + [title, titleInput, conversationId, showToast, refreshConversations, updateConvoMutation], + ); - const handleKeyDown = (e: KeyEvent) => { - if (e.key === 'Escape') { + const handleKeyDown = useCallback( + (e: KeyEvent) => { + if (e.key === 'Escape') { + setTitleInput(title); + setRenaming(false); + } else if (e.key === 'Enter') { + onRename(e); + } + }, + [title, onRename], + ); + + const cancelRename = useCallback( + (e: MouseEvent) => { + e.preventDefault(); setTitleInput(title); setRenaming(false); - } else if (e.key === 'Enter') { - onRename(e); - } - }; + }, + [title], + ); - const cancelRename = (e: MouseEvent) => { - e.preventDefault(); - setTitleInput(title); - setRenaming(false); - }; - - const isActiveConvo: boolean = - currentConvoId === conversationId || - (isLatestConvo && - currentConvoId === 'new' && - activeConvos[0] != null && - activeConvos[0] !== 'new'); + const isActiveConvo: boolean = useMemo( + () => + currentConvoId === conversationId || + (isLatestConvo && + currentConvoId === 'new' && + activeConvos[0] != null && + activeConvos[0] !== 'new'), + [currentConvoId, conversationId, isLatestConvo, activeConvos], + ); return (
{renaming ? ( -
+
- -
@@ -166,7 +189,7 @@ export default function Conversation({ onClick={clickHandler} className={cn( 'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2', - isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '', + isActiveConvo ? 'bg-surface-active-alt' : '', )} title={title ?? ''} > @@ -180,7 +203,7 @@ export default function Conversation({ {isActiveConvo ? (
) : ( -
+
)} )} @@ -193,12 +216,14 @@ export default function Conversation({ )} >
diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 59c63f896..e4f39c09e 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -2,6 +2,7 @@ import { useState, useId } from 'react'; import * as Ariakit from '@ariakit/react'; import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react'; import { useGetStartupConfig } from 'librechat-data-provider/react-query'; +import type { MouseEvent } from 'react'; import { useLocalize, useArchiveHandler } from '~/hooks'; import { DropdownPopup } from '~/components/ui'; import DeleteButton from './DeleteButton'; @@ -9,16 +10,26 @@ import ShareButton from './ShareButton'; import { cn } from '~/utils'; export default function ConvoOptions({ - conversation, + conversationId, + title, + renaming, retainView, renameHandler, isPopoverActive, setIsPopoverActive, isActiveConvo, +}: { + conversationId: string | null; + title: string | null; + renaming: boolean; + retainView: () => void; + renameHandler: (e: MouseEvent) => void; + isPopoverActive: boolean; + setIsPopoverActive: React.Dispatch>; + isActiveConvo: boolean; }) { const localize = useLocalize(); const { data: startupConfig } = useGetStartupConfig(); - const { conversationId, title } = conversation; const [showShareDialog, setShowShareDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const archiveHandler = useArchiveHandler(conversationId, true, retainView); @@ -73,6 +84,7 @@ export default function ConvoOptions({ isActiveConvo === true ? 'opacity-100' : 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100', + renaming === true ? 'pointer-events-none opacity-0' : '', )} > @@ -83,17 +95,17 @@ export default function ConvoOptions({ /> {showShareDialog && ( )} {showDeleteDialog && ( diff --git a/client/src/components/Tools/ToolItem.tsx b/client/src/components/Tools/ToolItem.tsx index c1995284d..776b5649d 100644 --- a/client/src/components/Tools/ToolItem.tsx +++ b/client/src/components/Tools/ToolItem.tsx @@ -1,5 +1,5 @@ import { TPlugin } from 'librechat-data-provider'; -import { XCircle, PlusCircleIcon } from 'lucide-react'; +import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react'; import { useLocalize } from '~/hooks'; type ToolItemProps = { @@ -9,7 +9,7 @@ type ToolItemProps = { isInstalled?: boolean; }; -function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps) { +function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolItemProps) { const localize = useLocalize(); const handleClick = () => { if (isInstalled) { @@ -20,20 +20,26 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps) }; return ( -
+
- {`${tool.name} + {tool.icon != null && tool.icon ? ( + {localize('com_ui_logo', + ) : ( +
+ +
+ )}
-
+
{tool.name}
{!isInstalled ? ( @@ -61,9 +67,7 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps) )}
-
- {tool.description} -
+
{tool.description}
); } diff --git a/client/src/components/Tools/ToolSelectDialog.tsx b/client/src/components/Tools/ToolSelectDialog.tsx index a2a1dc79c..708e4de8d 100644 --- a/client/src/components/Tools/ToolSelectDialog.tsx +++ b/client/src/components/Tools/ToolSelectDialog.tsx @@ -151,22 +151,22 @@ function ToolSelectDialog({ className="relative z-[102]" > {/* The backdrop, rendered as a fixed sibling to the panel container */} -
+
{/* Full-screen container to center the panel */}
-
+
- + {isAgentTools ? localize('com_nav_tool_dialog_agents') : localize('com_nav_tool_dialog')} - + {localize('com_nav_tool_dialog_description')}
@@ -178,7 +178,7 @@ function ToolSelectDialog({ setIsOpen(false); setCurrentPage(1); }} - className="inline-block text-gray-500 hover:text-gray-200" + className="inline-block text-text-tertiary hover:text-text-secondary" tabIndex={0} > @@ -206,13 +206,13 @@ function ToolSelectDialog({
- +
void; + onClick?: (e: React.MouseEvent) => void; icon?: React.ReactNode; kbd?: string; show?: boolean; @@ -69,7 +69,7 @@ const DropdownPopup: React.FC = ({ onClick={(event) => { event.preventDefault(); if (item.onClick) { - item.onClick(); + item.onClick(event); } menu.hide(); }} diff --git a/client/src/hooks/Conversations/useArchiveHandler.ts b/client/src/hooks/Conversations/useArchiveHandler.ts index 884755471..a0a1fd742 100644 --- a/client/src/hooks/Conversations/useArchiveHandler.ts +++ b/client/src/hooks/Conversations/useArchiveHandler.ts @@ -8,7 +8,7 @@ import useLocalize from '../useLocalize'; import useNewConvo from '../useNewConvo'; export default function useArchiveHandler( - conversationId: string, + conversationId: string | null, shouldArchive: boolean, retainView: () => void, ) { @@ -19,18 +19,22 @@ export default function useArchiveHandler( const { refreshConversations } = useConversations(); const { conversationId: currentConvoId } = useParams(); - const archiveConvoMutation = useArchiveConversationMutation(conversationId); + const archiveConvoMutation = useArchiveConversationMutation(conversationId ?? ''); return async (e?: MouseEvent | FocusEvent | KeyboardEvent) => { if (e) { e.preventDefault(); } + const convoId = conversationId ?? ''; + if (!convoId) { + return; + } const label = shouldArchive ? 'archive' : 'unarchive'; archiveConvoMutation.mutate( - { conversationId, isArchived: shouldArchive }, + { conversationId: convoId, isArchived: shouldArchive }, { onSuccess: () => { - if (currentConvoId === conversationId || currentConvoId === 'new') { + if (currentConvoId === convoId || currentConvoId === 'new') { newConversation(); navigate('/c/new', { replace: true }); } diff --git a/client/src/style.css b/client/src/style.css index 8a7987f23..88ee2c26e 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -48,6 +48,7 @@ html { --header-hover: var(--gray-50); --header-button-hover: var(--gray-50); --surface-active: var(--gray-100); + --surface-active-alt: var(--gray-200); --surface-hover: var(--gray-200); --surface-primary: var(--white); --surface-primary-alt: var(--gray-50); @@ -99,6 +100,7 @@ html { --header-hover: var(--gray-600); --header-button-hover: var(--gray-700); --surface-active: var(--gray-500); + --surface-active-alt: var(--gray-700); --surface-hover: var(--gray-600); --surface-primary: var(--gray-900); --surface-primary-alt: var(--gray-850); diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index 3dc4f32bd..de5537747 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -70,6 +70,7 @@ module.exports = { 'header-hover': 'var(--header-hover)', 'header-button-hover': 'var(--header-button-hover)', 'surface-active': 'var(--surface-active)', + 'surface-active-alt': 'var(--surface-active-alt)', 'surface-hover': 'var(--surface-hover)', 'surface-primary': 'var(--surface-primary)', 'surface-primary-alt': 'var(--surface-primary-alt)', diff --git a/config/packages.js b/config/packages.js index 0b457a7bf..b09bc6e66 100644 --- a/config/packages.js +++ b/config/packages.js @@ -9,6 +9,7 @@ const rootDir = path.resolve(__dirname, '..'); const directories = [ rootDir, path.resolve(rootDir, 'packages', 'data-provider'), + path.resolve(rootDir, 'packages', 'mcp'), path.resolve(rootDir, 'client'), path.resolve(rootDir, 'api'), ]; diff --git a/config/update.js b/config/update.js index 826624a01..0130d8190 100644 --- a/config/update.js +++ b/config/update.js @@ -16,6 +16,7 @@ const rootDir = path.resolve(__dirname, '..'); const directories = [ rootDir, path.resolve(rootDir, 'packages', 'data-provider'), + path.resolve(rootDir, 'packages', 'mcp'), path.resolve(rootDir, 'client'), path.resolve(rootDir, 'api'), ]; diff --git a/package-lock.json b/package-lock.json index 758ec3982..d86610651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "@langchain/google-genai": "^0.1.4", "@langchain/google-vertexai": "^0.1.2", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^1.8.5", + "@librechat/agents": "^1.8.8", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", @@ -65,7 +65,7 @@ "cors": "^2.8.5", "dedent": "^1.5.3", "dotenv": "^16.0.3", - "express": "^4.21.1", + "express": "^4.21.2", "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.4.1", "express-session": "^1.18.1", @@ -82,6 +82,7 @@ "klona": "^2.0.6", "langchain": "^0.2.19", "librechat-data-provider": "*", + "librechat-mcp": "*", "lodash": "^4.17.21", "meilisearch": "^0.38.0", "mime": "^3.0.0", @@ -142,6 +143,23 @@ "node": ">=18.0.0" } }, + "api/node_modules/@langchain/anthropic": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.11.tgz", + "integrity": "sha512-rYjDZjMwVQ+cYeJd9IoSESdkkG8fc0m3siGRYKNy6qgYMnqCz8sUPKBanXwbZAs6wvspPCGgNK9WONfaCeX97A==", + "dependencies": { + "@anthropic-ai/sdk": "^0.32.1", + "fast-xml-parser": "^4.4.1", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, "api/node_modules/@langchain/community": { "version": "0.3.14", "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.14.tgz", @@ -679,14 +697,14 @@ } }, "api/node_modules/@librechat/agents": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-1.8.5.tgz", - "integrity": "sha512-c7aVohA6RRISq67gqRz+iSAIVZag1ggfhfCpFw9cTCtZsJ2zUhvBwDw8+sN9yx5SLQ3LM/3Ns147529i7SfwvA==", + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-1.8.8.tgz", + "integrity": "sha512-8BM/MeyNMh4wlUIiswN7AfSZZQF2ibVOSiBhmA5PZfo314w/JnkivFNhRnAIh4yjd0ziGIgL2zHB7DRWAPnWSw==", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/credential-provider-node": "^3.613.0", "@aws-sdk/types": "^3.609.0", - "@langchain/anthropic": "^0.3.8", + "@langchain/anthropic": "^0.3.11", "@langchain/aws": "^0.1.2", "@langchain/community": "^0.3.14", "@langchain/core": "^0.3.18", @@ -742,59 +760,6 @@ "node": ">= 0.8.0" } }, - "api/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "api/node_modules/express-rate-limit": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", @@ -813,6 +778,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "extraneous": true, "engines": { "node": ">= 0.6" } @@ -936,11 +902,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "api/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "api/node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -6241,6 +6202,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", @@ -9224,6 +9207,8 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.8.tgz", "integrity": "sha512-7qeRDhNnCf1peAbjY825R2HNszobJeGvqi2cfPl+YsduDIYEGUzfoGRRarPI5joIGX5YshCsch6NFtap2bLfmw==", + "optional": true, + "peer": true, "dependencies": { "@anthropic-ai/sdk": "^0.27.3", "fast-xml-parser": "^4.4.1", @@ -9241,6 +9226,8 @@ "version": "0.27.3", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.27.3.tgz", "integrity": "sha512-IjLt0gd3L4jlOfilxVXTifn42FnVffMgDC04RJK1KDZpmkBWLv0XC92MVVmkxrFZNS/7l3xWgP/I3nqtX1sQHw==", + "optional": true, + "peer": true, "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -9255,6 +9242,8 @@ "version": "18.19.64", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz", "integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -9995,12 +9984,12 @@ } }, "node_modules/@langchain/langgraph": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.20.tgz", - "integrity": "sha512-MMD4G++gHs+5OO5Uu75gduskTboJ8Q7ZAwzd1s64a1Y/38pdgDqJdYRHRCGpx8eeCuKhsRzV2Sssnl5lujfj8w==", + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.33.tgz", + "integrity": "sha512-Tx2eU98XicIOoZzRkzQqLxZrF93B9xONYmWSq3kfDUoC0nzQbkydpygF1MTcUM9hKPQsSGMBrxgXht5+sNXzYg==", "dependencies": { - "@langchain/langgraph-checkpoint": "~0.0.10", - "@langchain/langgraph-sdk": "~0.0.20", + "@langchain/langgraph-checkpoint": "~0.0.13", + "@langchain/langgraph-sdk": "~0.0.21", "uuid": "^10.0.0", "zod": "^3.23.8" }, @@ -10012,9 +10001,9 @@ } }, "node_modules/@langchain/langgraph-checkpoint": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.11.tgz", - "integrity": "sha512-nroHHkAi/UPn9LqqZcgOydfB8qZw5TXuXDFc43MIydnW4lb8m9hVHnQ3lgb2WGSgtbZJnsIx0TzL19oemJBRKg==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.13.tgz", + "integrity": "sha512-amdmBcNT8a9xP2VwcEWxqArng4gtRDcnVyVI4DsQIo1Aaz8e8+hH17zSwrUF3pt1pIYztngIfYnBOim31mtKMg==", "dependencies": { "uuid": "^10.0.0" }, @@ -10038,9 +10027,9 @@ } }, "node_modules/@langchain/langgraph-sdk": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.23.tgz", - "integrity": "sha512-4LfwMN1PdawJ9I3dXxQHUb1NoJaZo5SklQbAamrS6fLrUU9fSoYkPu1mYQp3uJjKtXRYOnuoP0egYyQPoKuiXQ==", + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.32.tgz", + "integrity": "sha512-KQyM9kLO7T6AxwNrceajH7JOybP3pYpvUPnhiI2rrVndI1WyZUJ1eVC1e722BVRAPi6o+WcoTT4uMSZVinPOtA==", "dependencies": { "@types/json-schema": "^7.0.15", "p-queue": "^6.6.2", @@ -10418,6 +10407,41 @@ "node-fetch": "^2.6.7" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.3.tgz", + "integrity": "sha512-2as3cX/VJ0YBHGmdv3GFyTpoM8q2gqE98zh3Vf1NwnsSY0h3mvoO07MUzfygCKkWsFjcZm4otIiqD6Xh7kiSBQ==", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", @@ -14466,6 +14490,30 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -14513,6 +14561,25 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -14521,6 +14588,12 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz", + "integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -14544,6 +14617,30 @@ "@types/estree": "*" } }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -14561,6 +14658,12 @@ "@types/unist": "^2" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -14604,7 +14707,8 @@ "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true }, "node_modules/@types/jsdom": { "version": "20.0.1", @@ -14649,6 +14753,12 @@ "@types/unist": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -14688,6 +14798,18 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "node_modules/@types/react": { "version": "18.2.53", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.53.tgz", @@ -14729,6 +14851,27 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -14784,6 +14927,16 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/winston": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", + "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", + "deprecated": "This is a stub types definition. winston provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "winston": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -16152,7 +16305,9 @@ "node_modules/base-64": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", + "optional": true, + "peer": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -16806,6 +16961,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -17620,7 +17777,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/crelt": { "version": "1.0.6", @@ -17669,6 +17826,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "optional": true, + "peer": true, "engines": { "node": "*" } @@ -18257,6 +18416,14 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -18285,6 +18452,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "optional": true, + "peer": true, "dependencies": { "base-64": "^0.1.0", "md5": "^2.3.0" @@ -19470,6 +19639,25 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.1.tgz", + "integrity": "sha512-tyGtsrTc9fi+N5qFU6G2MLjcBbsdCOQ/QE9Cc96Mt6q02YkQrIJGOaNMg6qiXRJDzxecN7BntJYNRE/j0OIhMQ==", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -19544,6 +19732,51 @@ "resolved": "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz", "integrity": "sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==" }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/express-mongo-sanitize": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/express-mongo-sanitize/-/express-mongo-sanitize-2.2.0.tgz", @@ -19588,6 +19821,32 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -22269,7 +22528,9 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true, + "peer": true }, "node_modules/is-builtin-module": { "version": "3.2.1", @@ -24453,6 +24714,10 @@ "resolved": "packages/data-provider", "link": true }, + "node_modules/librechat-mcp": { + "resolved": "packages/mcp", + "link": true + }, "node_modules/light-my-request": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", @@ -25299,7 +25564,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/make-plural": { "version": "7.3.0", @@ -25355,6 +25620,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "optional": true, + "peer": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -26839,9 +27106,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -27945,6 +28212,8 @@ "version": "4.11.1", "resolved": "https://registry.npmjs.org/openai/-/openai-4.11.1.tgz", "integrity": "sha512-GU0HQWbejXuVAQlDjxIE8pohqnjptFDIm32aPlNT1H9ucMz1VJJD0DaTJRQsagNaJ97awWjjVLEG7zCM6sm4SA==", + "optional": true, + "peer": true, "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -27971,6 +28240,8 @@ "version": "18.19.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -28232,6 +28503,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -28495,15 +28772,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -28517,11 +28794,6 @@ "node": "14 || >=16.14" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -33976,6 +34248,64 @@ } } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -34184,7 +34514,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -34697,6 +35027,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -36114,6 +36450,15 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -36135,11 +36480,11 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.22.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz", - "integrity": "sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q==", + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz", + "integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==", "peerDependencies": { - "zod": "^3.22.4" + "zod": "^3.23.3" } }, "node_modules/zwitch": { @@ -36153,14 +36498,11 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.62", + "version": "0.7.63", "license": "ISC", "dependencies": { - "@types/js-yaml": "^4.0.9", "axios": "^1.7.7", "js-yaml": "^4.1.0", - "openai": "4.11.1", - "openapi-types": "^12.1.3", "zod": "^3.22.4" }, "devDependencies": { @@ -36174,10 +36516,13 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@types/jest": "^29.5.2", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.3.0", "@types/react": "^18.2.18", "jest": "^29.5.0", "jest-junit": "^16.0.0", + "openai": "^4.76.3", + "openapi-types": "^12.1.3", "rimraf": "^5.0.1", "rollup": "^4.22.4", "rollup-plugin-generate-package-json": "^3.2.0", @@ -36235,6 +36580,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/data-provider/node_modules/openai": { + "version": "4.76.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.76.3.tgz", + "integrity": "sha512-BISkI90m8zT7BAMljK0j00TzOoLvmc7AulPxv6EARa++3+hhIK5G6z4xkITurEaA9bvDhQ09kSNKA3DL+rDMwA==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "packages/data-provider/node_modules/openai/node_modules/@types/node": { + "version": "18.19.68", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", + "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "packages/data-provider/node_modules/rimraf": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", @@ -36252,6 +36632,118 @@ "funding": { "url": "https://github.com/sponsors/isaacs" } + }, + "packages/mcp": { + "name": "librechat-mcp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.3", + "diff": "^7.0.0", + "eventsource": "^3.0.1", + "express": "^4.21.2" + }, + "devDependencies": { + "@babel/preset-env": "^7.21.5", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.1.0", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-terser": "^0.4.4", + "@types/diff": "^6.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.0", + "@types/react": "^18.2.18", + "@types/winston": "^2.4.4", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "librechat-data-provider": "*", + "rimraf": "^5.0.1", + "rollup": "^4.22.4", + "rollup-plugin-generate-package-json": "^3.2.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-typescript2": "^0.35.0", + "ts-node": "^10.9.2", + "typescript": "^5.0.4" + } + }, + "packages/mcp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/mcp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/mcp/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "packages/mcp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/mcp/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } } } } diff --git a/package.json b/package.json index 213195184..d08908cb0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "backend:dev": "cross-env NODE_ENV=development npx nodemon api/server/index.js", "backend:stop": "node config/stop-backend.js", "build:data-provider": "cd packages/data-provider && npm run build", - "frontend": "npm run build:data-provider && cd client && npm run build", + "build:mcp": "cd packages/mcp && npm run build", + "frontend": "npm run build:data-provider && npm run build:mcp && cd client && npm run build", "frontend:ci": "npm run build:data-provider && cd client && npm run build:ci", "frontend:dev": "cd client && npm run dev", "e2e": "playwright test --config=e2e/playwright.config.local.ts", diff --git a/packages/data-provider/check_updates.sh b/packages/data-provider/check_updates.sh index 8ee7c109d..2daebb030 100755 --- a/packages/data-provider/check_updates.sh +++ b/packages/data-provider/check_updates.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SCRIPT USED TO DETERMINE WHICH PACKAGE HAD CHANGES # Set the directory containing the package.json file dir=${1:-.} diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index ba3792684..dcdfc0e12 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.62", + "version": "0.7.63", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", @@ -39,11 +39,8 @@ }, "homepage": "https://librechat.ai", "dependencies": { - "@types/js-yaml": "^4.0.9", "axios": "^1.7.7", "js-yaml": "^4.1.0", - "openai": "4.11.1", - "openapi-types": "^12.1.3", "zod": "^3.22.4" }, "devDependencies": { @@ -57,10 +54,13 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@types/jest": "^29.5.2", + "@types/js-yaml": "^4.0.9", "@types/node": "^20.3.0", "@types/react": "^18.2.18", "jest": "^29.5.0", "jest-junit": "^16.0.0", + "openai": "^4.76.3", + "openapi-types": "^12.1.3", "rimraf": "^5.0.1", "rollup": "^4.22.4", "rollup-plugin-generate-package-json": "^3.2.0", diff --git a/packages/data-provider/server-rollup.config.js b/packages/data-provider/server-rollup.config.js index fc83037d4..d198e4582 100644 --- a/packages/data-provider/server-rollup.config.js +++ b/packages/data-provider/server-rollup.config.js @@ -10,7 +10,7 @@ const entryPath = path.resolve(rootPath, 'api/server/index.js'); console.log('entryPath', entryPath); -// Define your custom aliases here +// Define custom aliases here const customAliases = { entries: [{ find: '~', replacement: rootServerPath }], }; @@ -18,7 +18,7 @@ const customAliases = { export default { input: entryPath, output: { - file: 'test_bundle/bundle.js', + dir: 'test_bundle', format: 'cjs', }, plugins: [ diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 506fe6b3f..60d5c2037 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1,11 +1,12 @@ /* eslint-disable max-len */ import { z } from 'zod'; import type { ZodError } from 'zod'; +import type { TModelsConfig } from './types'; import { EModelEndpoint, eModelEndpointSchema } from './schemas'; import { fileConfigSchema } from './file-config'; import { specsConfigSchema } from './models'; import { FileSources } from './types/files'; -import { TModelsConfig } from './types'; +import { MCPServersSchema } from './mcp'; export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord']; @@ -432,6 +433,7 @@ export const configSchema = z.object({ imageOutputType: z.nativeEnum(EImageOutputType).default(EImageOutputType.PNG), includedTools: z.array(z.string()).optional(), filteredTools: z.array(z.string()).optional(), + mcpServers: MCPServersSchema.optional(), interface: z .object({ privacyPolicy: z @@ -1086,7 +1088,7 @@ export enum Constants { /** Key for the app's version. */ VERSION = 'v0.7.5', /** Key for the Custom Config's version (librechat.yaml). */ - CONFIG_VERSION = '1.1.9', + CONFIG_VERSION = '1.2.0', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ NO_PARENT = '00000000-0000-0000-0000-000000000000', /** Standard value for the initial conversationId before a request is sent */ @@ -1109,6 +1111,8 @@ export enum Constants { MAX_CONVO_STARTERS = 4, /** Global/instance Project Name */ GLOBAL_PROJECT_NAME = 'instance', + /** Delimiter for MCP tools */ + mcp_delimiter = '_mcp_', } export enum LocalStorageKeys { diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index c42857bd9..739ece733 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -11,6 +11,8 @@ export * from './zod'; /* custom/dynamic configurations */ export * from './generate'; export * from './models'; +/* mcp */ +export * from './mcp'; /* RBAC */ export * from './roles'; /* types (exports schemas from `./types` as they contain needed in other defs) */ diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts new file mode 100644 index 000000000..bb8a55f16 --- /dev/null +++ b/packages/data-provider/src/mcp.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; + +const BaseOptionsSchema = z.object({ + iconPath: z.string().optional(), +}); + +export const StdioOptionsSchema = BaseOptionsSchema.extend({ + type: z.literal('stdio').optional(), + /** + * The executable to run to start the server. + */ + command: z.string(), + /** + * Command line arguments to pass to the executable. + */ + args: z.array(z.string()), + /** + * The environment to use when spawning the process. + * + * If not specified, the result of getDefaultEnvironment() will be used. + */ + env: z.record(z.string(), z.string()).optional(), + /** + * How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`. + * + * @type {import('node:child_process').IOType | import('node:stream').Stream | number} + * + * The default is "inherit", meaning messages to stderr will be printed to the parent process's stderr. + */ + stderr: z.any().optional(), +}); + +export const WebSocketOptionsSchema = BaseOptionsSchema.extend({ + type: z.literal('websocket').optional(), + url: z + .string() + .url() + .refine( + (val) => { + const protocol = new URL(val).protocol; + return protocol === 'ws:' || protocol === 'wss:'; + }, + { + message: 'WebSocket URL must start with ws:// or wss://', + }, + ), +}); + +export const SSEOptionsSchema = BaseOptionsSchema.extend({ + type: z.literal('sse').optional(), + url: z + .string() + .url() + .refine( + (val) => { + const protocol = new URL(val).protocol; + return protocol !== 'ws:' && protocol !== 'wss:'; + }, + { + message: 'SSE URL must not start with ws:// or wss://', + }, + ), +}); + +export const MCPOptionsSchema = z.union([ + StdioOptionsSchema, + WebSocketOptionsSchema, + SSEOptionsSchema, +]); + +export const MCPServersSchema = z.record(z.string(), MCPOptionsSchema); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 70228d196..0af144f95 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -369,7 +369,7 @@ export const tPluginSchema = z.object({ name: z.string(), pluginKey: z.string(), description: z.string(), - icon: z.string(), + icon: z.string().optional(), authConfig: z.array(tPluginAuthConfigSchema).optional(), authenticated: z.boolean().optional(), isButton: z.boolean().optional(), diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 4244d73fa..5e846e4d4 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -1,4 +1,4 @@ -import OpenAI from 'openai'; +import type OpenAI from 'openai'; import type { InfiniteData } from '@tanstack/react-query'; import type { TMessage, @@ -12,8 +12,6 @@ import type { } from './schemas'; import type { TSpecsConfig } from './models'; export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam; -export type TOpenAIFunction = OpenAI.Chat.ChatCompletionCreateParams.Function; -export type TOpenAIFunctionCall = OpenAI.Chat.ChatCompletionCreateParams.FunctionCallOption; export * from './schemas'; diff --git a/packages/mcp/.gitignore b/packages/mcp/.gitignore new file mode 100644 index 000000000..7b961825b --- /dev/null +++ b/packages/mcp/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +test_bundle/ diff --git a/packages/mcp/babel.config.js b/packages/mcp/babel.config.js new file mode 100644 index 000000000..7d5344d25 --- /dev/null +++ b/packages/mcp/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], + plugins: ['babel-plugin-replace-ts-export-assignment'], +}; diff --git a/packages/mcp/jest.config.js b/packages/mcp/jest.config.js new file mode 100644 index 000000000..6b8c4abe7 --- /dev/null +++ b/packages/mcp/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!/node_modules/'], + coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + coverageReporters: ['text', 'cobertura'], + testResultsProcessor: 'jest-junit', + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + }, + // coverageThreshold: { + // global: { + // statements: 58, + // branches: 49, + // functions: 50, + // lines: 57, + // }, + // }, + restoreMocks: true, +}; diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 000000000..1ab924f01 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,77 @@ +{ + "name": "librechat-mcp", + "version": "1.0.0", + "type": "module", + "description": "MCP services for LibreChat", + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.es.js", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && rollup -c --silent --bundleConfigAsCjs", + "build:watch": "rollup -c -w", + "rollup:api": "npx rollup -c server-rollup.config.js --bundleConfigAsCjs", + "test": "jest --coverage --watch", + "test:ci": "jest --coverage --ci", + "verify": "npm run test:ci", + "b:clean": "bun run rimraf dist", + "b:build": "bun run b:clean && bun run rollup -c --silent --bundleConfigAsCjs", + "start:everything-sse": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/examples/everything/sse.ts", + "start:everything": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/everything.ts", + "start:filesystem": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/filesystem.ts", + "start:servers": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/servers.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/danny-avila/LibreChat.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/danny-avila/LibreChat/issues" + }, + "homepage": "https://librechat.ai", + "devDependencies": { + "@babel/preset-env": "^7.21.5", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.1.0", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-terser": "^0.4.4", + "@types/diff": "^6.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.0", + "@types/react": "^18.2.18", + "@types/winston": "^2.4.4", + "jest": "^29.5.0", + "jest-junit": "^16.0.0", + "librechat-data-provider": "*", + "rimraf": "^5.0.1", + "rollup": "^4.22.4", + "rollup-plugin-generate-package-json": "^3.2.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-typescript2": "^0.35.0", + "ts-node": "^10.9.2", + "typescript": "^5.0.4" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.3", + "diff": "^7.0.0", + "eventsource": "^3.0.1", + "express": "^4.21.2" + } +} diff --git a/packages/mcp/rollup.config.js b/packages/mcp/rollup.config.js new file mode 100644 index 000000000..4930d69d4 --- /dev/null +++ b/packages/mcp/rollup.config.js @@ -0,0 +1,46 @@ +import typescript from 'rollup-plugin-typescript2'; +import resolve from '@rollup/plugin-node-resolve'; +import pkg from './package.json'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import commonjs from '@rollup/plugin-commonjs'; +import replace from '@rollup/plugin-replace'; +import terser from '@rollup/plugin-terser'; + +const plugins = [ + peerDepsExternal(), + resolve(), + replace({ + __IS_DEV__: process.env.NODE_ENV === 'development', + }), + commonjs(), + typescript({ + tsconfig: './tsconfig.json', + useTsconfigDeclarationDir: true, + }), + terser(), +]; + +export default [ + { + input: 'src/index.ts', + output: [ + { + file: pkg.main, + format: 'cjs', + sourcemap: true, + exports: 'named', + }, + { + file: pkg.module, + format: 'esm', + sourcemap: true, + exports: 'named', + }, + ], + ...{ + external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})], + preserveSymlinks: true, + plugins, + }, + }, +]; diff --git a/packages/mcp/server-rollup.config.js b/packages/mcp/server-rollup.config.js new file mode 100644 index 000000000..4a04b610e --- /dev/null +++ b/packages/mcp/server-rollup.config.js @@ -0,0 +1,40 @@ +import path from 'path'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import alias from '@rollup/plugin-alias'; +import json from '@rollup/plugin-json'; + +const rootPath = path.resolve(__dirname, '../../'); +const rootServerPath = path.resolve(__dirname, '../../api'); +const entryPath = path.resolve(rootPath, 'api/server/index.js'); + +console.log('entryPath', entryPath); + +// Define custom aliases here +const customAliases = { + entries: [{ find: '~', replacement: rootServerPath }], +}; + +export default { + input: entryPath, + output: { + file: 'test_bundle/bundle.js', + format: 'cjs', + }, + plugins: [ + alias(customAliases), + resolve({ + preferBuiltins: true, + extensions: ['.js', '.json', '.node'], + }), + commonjs(), + json(), + ], + external: (id) => { + // More selective external function + if (/node_modules/.test(id)) { + return !id.startsWith('langchain/'); + } + return false; + }, +}; diff --git a/packages/mcp/src/connection.ts b/packages/mcp/src/connection.ts new file mode 100644 index 000000000..ac860d1c4 --- /dev/null +++ b/packages/mcp/src/connection.ts @@ -0,0 +1,475 @@ +import { EventEmitter } from 'events'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js'; +import { ResourceListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { Logger } from 'winston'; +import type * as t from './types/mcp.js'; + +function isStdioOptions(options: t.MCPOptions): options is t.StdioOptions { + return 'command' in options; +} + +function isWebSocketOptions(options: t.MCPOptions): options is t.WebSocketOptions { + if ('url' in options) { + const protocol = new URL(options.url).protocol; + return protocol === 'ws:' || protocol === 'wss:'; + } + return false; +} + +function isSSEOptions(options: t.MCPOptions): options is t.SSEOptions { + if ('url' in options) { + const protocol = new URL(options.url).protocol; + return protocol !== 'ws:' && protocol !== 'wss:'; + } + return false; +} +export class MCPConnection extends EventEmitter { + private static instance: MCPConnection | null = null; + public client: Client; + private transport: Transport | null = null; // Make this nullable + private connectionState: t.ConnectionState = 'disconnected'; + private connectPromise: Promise | null = null; + private lastError: Error | null = null; + private lastConfigUpdate = 0; + private readonly CONFIG_TTL = 5 * 60 * 1000; // 5 minutes + private readonly MAX_RECONNECT_ATTEMPTS = 3; + public readonly serverName: string; + private shouldStopReconnecting = false; + private isReconnecting = false; + private isInitializing = false; + private reconnectAttempts = 0; + iconPath?: string; + + constructor(serverName: string, private readonly options: t.MCPOptions, private logger?: Logger) { + super(); + this.serverName = serverName; + this.logger = logger; + this.iconPath = options.iconPath; + this.client = new Client( + { + name: 'librechat-mcp-client', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + this.setupEventListeners(); + } + + public static getInstance( + serverName: string, + options: t.MCPOptions, + logger?: Logger, + ): MCPConnection { + if (!MCPConnection.instance) { + MCPConnection.instance = new MCPConnection(serverName, options, logger); + } + return MCPConnection.instance; + } + + public static getExistingInstance(): MCPConnection | null { + return MCPConnection.instance; + } + + public static async destroyInstance(): Promise { + if (MCPConnection.instance) { + await MCPConnection.instance.disconnect(); + MCPConnection.instance = null; + } + } + + private emitError(error: unknown, errorContext: string): void { + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger?.error(`[MCP][${this.serverName}] ${errorContext}: ${errorMessage}`); + this.emit('error', new Error(`${errorContext}: ${errorMessage}`)); + } + + private constructTransport(options: t.MCPOptions): Transport { + try { + let type: t.MCPOptions['type']; + if (isStdioOptions(options)) { + type = 'stdio'; + } else if (isWebSocketOptions(options)) { + type = 'websocket'; + } else if (isSSEOptions(options)) { + type = 'sse'; + } else { + throw new Error( + 'Cannot infer transport type: options.type is not provided and cannot be inferred from other properties.', + ); + } + + switch (type) { + case 'stdio': + if (!isStdioOptions(options)) { + throw new Error('Invalid options for stdio transport.'); + } + return new StdioClientTransport({ + command: options.command, + args: options.args, + env: options.env, + }); + + case 'websocket': + if (!isWebSocketOptions(options)) { + throw new Error('Invalid options for websocket transport.'); + } + return new WebSocketClientTransport(new URL(options.url)); + + case 'sse': { + if (!isSSEOptions(options)) { + throw new Error('Invalid options for sse transport.'); + } + const url = new URL(options.url); + this.logger?.info(`[MCP][${this.serverName}] Creating SSE transport: ${url.toString()}`); + const transport = new SSEClientTransport(url); + + transport.onclose = () => { + this.logger?.info(`[MCP][${this.serverName}] SSE transport closed`); + this.emit('connectionChange', 'disconnected'); + }; + + transport.onerror = (error) => { + this.logger?.error(`[MCP][${this.serverName}] SSE transport error:`, error); + this.emitError(error, 'SSE transport error:'); + }; + + transport.onmessage = (message) => { + this.logger?.info( + `[MCP][${this.serverName}] Message received: ${JSON.stringify(message)}`, + ); + }; + + this.setupTransportErrorHandlers(transport); + return transport; + } + + default: { + throw new Error(`Unsupported transport type: ${type}`); + } + } + } catch (error) { + this.emitError(error, 'Failed to construct transport:'); + throw error; + } + } + + private setupEventListeners(): void { + this.isInitializing = true; + this.on('connectionChange', (state: t.ConnectionState) => { + this.connectionState = state; + if (state === 'connected') { + this.isReconnecting = false; + this.isInitializing = false; + this.shouldStopReconnecting = false; + this.reconnectAttempts = 0; + } else if (state === 'error' && !this.isReconnecting && !this.isInitializing) { + this.handleReconnection().catch((error) => { + this.logger?.error(`[MCP][${this.serverName}] Reconnection handler failed:`, error); + }); + } + }); + + this.subscribeToResources(); + } + + private async handleReconnection(): Promise { + if (this.isReconnecting || this.shouldStopReconnecting || this.isInitializing) { + return; + } + + this.isReconnecting = true; + const backoffDelay = (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 30000); + + try { + while ( + this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS && + !(this.shouldStopReconnecting as boolean) + ) { + this.reconnectAttempts++; + const delay = backoffDelay(this.reconnectAttempts); + + this.logger?.info( + `[MCP][${this.serverName}] Reconnecting ${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS} (delay: ${delay}ms)`, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + try { + await this.connect(); + this.reconnectAttempts = 0; + return; + } catch (error) { + this.logger?.error(`[MCP][${this.serverName}] Reconnection attempt failed:`, error); + + if ( + this.reconnectAttempts === this.MAX_RECONNECT_ATTEMPTS || + (this.shouldStopReconnecting as boolean) + ) { + this.logger?.error(`[MCP][${this.serverName}] Stopping reconnection attempts`); + return; + } + } + } + } finally { + this.isReconnecting = false; + } + } + + private subscribeToResources(): void { + this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => { + this.invalidateCache(); + this.emit('resourcesChanged'); + }); + } + + private invalidateCache(): void { + // this.cachedConfig = null; + this.lastConfigUpdate = 0; + } + + async connectClient(): Promise { + if (this.connectionState === 'connected') { + return; + } + + if (this.connectPromise) { + return this.connectPromise; + } + + if (this.shouldStopReconnecting) { + return; + } + + this.emit('connectionChange', 'connecting'); + + this.connectPromise = (async () => { + try { + if (this.transport) { + try { + await this.client.close(); + this.transport = null; + } catch (error) { + this.logger?.warn(`[MCP][${this.serverName}] Error closing connection:`, error); + } + } + + this.transport = this.constructTransport(this.options); + this.setupTransportDebugHandlers(); + + const connectTimeout = 10000; + await Promise.race([ + this.client.connect(this.transport), + new Promise((_resolve, reject) => + setTimeout(() => reject(new Error('Connection timeout')), connectTimeout), + ), + ]); + + this.connectionState = 'connected'; + this.emit('connectionChange', 'connected'); + this.reconnectAttempts = 0; + } catch (error) { + this.connectionState = 'error'; + this.emit('connectionChange', 'error'); + this.lastError = error instanceof Error ? error : new Error(String(error)); + throw error; + } finally { + this.connectPromise = null; + } + })(); + + return this.connectPromise; + } + + private setupTransportDebugHandlers(): void { + if (!this.transport) { + return; + } + + this.transport.onmessage = (msg) => { + this.logger?.debug(`[MCP][${this.serverName}] Transport received: ${JSON.stringify(msg)}`); + }; + + const originalSend = this.transport.send.bind(this.transport); + this.transport.send = async (msg) => { + this.logger?.debug(`[MCP][${this.serverName}] Transport sending: ${JSON.stringify(msg)}`); + return originalSend(msg); + }; + } + + async connect(): Promise { + try { + await this.disconnect(); + await this.connectClient(); + if (!this.isConnected()) { + throw new Error('Connection not established'); + } + } catch (error) { + this.logger?.error(`[MCP][${this.serverName}] Connection failed:`, error); + throw error; + } + } + + private setupTransportErrorHandlers(transport: Transport): void { + transport.onerror = (error) => { + this.logger?.error(`[MCP][${this.serverName}] Transport error:`, error); + this.emit('connectionChange', 'error'); + }; + + const errorHandler = (error: Error) => { + try { + this.logger?.error(`[MCP][${this.serverName}] Uncaught transport error:`, error); + } catch { + console.error(`[MCP][${this.serverName}] Critical error logging failed`, error); + } + this.emit('connectionChange', 'error'); + }; + + process.on('uncaughtException', errorHandler); + process.on('unhandledRejection', errorHandler); + } + + public async disconnect(): Promise { + try { + if (this.transport) { + await this.client.close(); + this.transport = null; + } + if (this.connectionState === 'disconnected') { + return; + } + this.connectionState = 'disconnected'; + this.emit('connectionChange', 'disconnected'); + } catch (error) { + this.emit('error', error); + throw error; + } finally { + this.invalidateCache(); + this.connectPromise = null; + } + } + + async fetchResources(): Promise { + try { + const { resources } = await this.client.listResources(); + return resources; + } catch (error) { + this.emitError(error, 'Failed to fetch resources:'); + return []; + } + } + + async fetchTools() { + try { + const { tools } = await this.client.listTools(); + return tools; + } catch (error) { + this.emitError(error, 'Failed to fetch tools:'); + return []; + } + } + + async fetchPrompts(): Promise { + try { + const { prompts } = await this.client.listPrompts(); + return prompts; + } catch (error) { + this.emitError(error, 'Failed to fetch prompts:'); + return []; + } + } + + // public async modifyConfig(config: ContinueConfig): Promise { + // try { + // // Check cache + // if (this.cachedConfig && Date.now() - this.lastConfigUpdate < this.CONFIG_TTL) { + // return this.cachedConfig; + // } + + // await this.connectClient(); + + // // Fetch and process resources + // const resources = await this.fetchResources(); + // const submenuItems = resources.map(resource => ({ + // title: resource.name, + // description: resource.description, + // id: resource.uri, + // })); + + // if (!config.contextProviders) { + // config.contextProviders = []; + // } + + // config.contextProviders.push( + // new MCPContextProvider({ + // submenuItems, + // client: this.client, + // }), + // ); + + // // Fetch and process tools + // const tools = await this.fetchTools(); + // const continueTools: Tool[] = tools.map(tool => ({ + // displayTitle: tool.name, + // function: { + // description: tool.description, + // name: tool.name, + // parameters: tool.inputSchema, + // }, + // readonly: false, + // type: 'function', + // wouldLikeTo: `use the ${tool.name} tool`, + // uri: `mcp://${tool.name}`, + // })); + + // config.tools = [...(config.tools || []), ...continueTools]; + + // // Fetch and process prompts + // const prompts = await this.fetchPrompts(); + // if (!config.slashCommands) { + // config.slashCommands = []; + // } + + // const slashCommands: SlashCommand[] = prompts.map(prompt => + // constructMcpSlashCommand( + // this.client, + // prompt.name, + // prompt.description, + // prompt.arguments?.map(a => a.name), + // ), + // ); + // config.slashCommands.push(...slashCommands); + + // // Update cache + // this.cachedConfig = config; + // this.lastConfigUpdate = Date.now(); + + // return config; + // } catch (error) { + // this.emit('error', error); + // // Return original config if modification fails + // return config; + // } + // } + + // Public getters for state information + public getConnectionState(): t.ConnectionState { + return this.connectionState; + } + + public isConnected(): boolean { + return this.connectionState === 'connected'; + } + + public getLastError(): Error | null { + return this.lastError; + } +} diff --git a/packages/mcp/src/demo/everything.ts b/packages/mcp/src/demo/everything.ts new file mode 100644 index 000000000..58e677a3e --- /dev/null +++ b/packages/mcp/src/demo/everything.ts @@ -0,0 +1,231 @@ +import express from 'express'; +import { EventSource } from 'eventsource'; +import { MCPConnection } from '../connection'; +import type { MCPOptions } from '../types/mcp'; + +// Set up EventSource for Node environment +global.EventSource = EventSource; + +const app = express(); +app.use(express.json()); + +let mcp: MCPConnection; + +const initializeMCP = async () => { + console.log('Initializing MCP with SSE transport...'); + + const mcpOptions: MCPOptions = { + type: 'sse' as const, + url: 'http://localhost:3001/sse', + // type: 'stdio' as const, + // 'command': 'npx', + // 'args': [ + // '-y', + // '@modelcontextprotocol/server-everything', + // ], + }; + + try { + await MCPConnection.destroyInstance(); + mcp = MCPConnection.getInstance('everything', mcpOptions); + + mcp.on('connectionChange', (state) => { + console.log(`MCP connection state changed to: ${state}`); + }); + + mcp.on('error', (error) => { + console.error('MCP error:', error); + }); + + console.log('Connecting to MCP server...'); + await mcp.connectClient(); + console.log('Connected to MCP server'); + + // Test the connection + try { + const resources = await mcp.fetchResources(); + console.log('Available resources:', resources); + } catch (error) { + console.error('Error fetching resources:', error); + } + } catch (error) { + console.error('Failed to connect to MCP server:', error); + } +}; + +// API Endpoints +app.get('/status', (req, res) => { + res.json({ + connected: mcp.isConnected(), + state: mcp.getConnectionState(), + error: mcp.getLastError()?.message, + }); +}); + +// Resources endpoint +app.get('/resources', async (req, res) => { + try { + const resources = await mcp.fetchResources(); + res.json({ resources }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Tools endpoint with all tool operations +app.get('/tools', async (req, res) => { + try { + const tools = await mcp.fetchTools(); + res.json({ tools }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Echo tool endpoint +app.post('/tools/echo', async (req, res) => { + try { + const { message } = req.body; + const result = await mcp.client.callTool({ + name: 'echo', + arguments: { message }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Add tool endpoint +app.post('/tools/add', async (req, res) => { + try { + const { a, b } = req.body; + const result = await mcp.client.callTool({ + name: 'add', + arguments: { a, b }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Long running operation endpoint +app.post('/tools/long-operation', async (req, res) => { + try { + const { duration, steps } = req.body; + const result = await mcp.client.callTool({ + name: 'longRunningOperation', + arguments: { duration, steps }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Sample LLM endpoint +app.post('/tools/sample', async (req, res) => { + try { + const { prompt, maxTokens } = req.body; + const result = await mcp.client.callTool({ + name: 'sampleLLM', + arguments: { prompt, maxTokens }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Get tiny image endpoint +app.get('/tools/tiny-image', async (req, res) => { + try { + const result = await mcp.client.callTool({ + name: 'getTinyImage', + arguments: {}, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Prompts endpoints +app.get('/prompts', async (req, res) => { + try { + const prompts = await mcp.fetchPrompts(); + res.json({ prompts }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +app.post('/prompts/simple', async (req, res) => { + try { + const result = await mcp.client.getPrompt({ + name: 'simple_prompt', + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +app.post('/prompts/complex', async (req, res) => { + try { + const { temperature, style } = req.body; + const result = await mcp.client.getPrompt({ + name: 'complex_prompt', + arguments: { temperature, style }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Resource subscription endpoints +app.post('/resources/subscribe', async (req, res) => { + try { + const { uri } = req.body; + await mcp.client.subscribeResource({ uri }); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +app.post('/resources/unsubscribe', async (req, res) => { + try { + const { uri } = req.body; + await mcp.client.unsubscribeResource({ uri }); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Error handling +// eslint-disable-next-line @typescript-eslint/no-unused-vars +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Unhandled error:', err); + res.status(500).json({ + error: 'Internal server error', + message: err.message, + }); +}); + +// Cleanup on shutdown +process.on('SIGINT', async () => { + console.log('Shutting down...'); + await MCPConnection.destroyInstance(); + process.exit(0); +}); + +// Start server +const PORT = process.env.MCP_PORT ?? 3000; +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + initializeMCP(); +}); diff --git a/packages/mcp/src/demo/filesystem.ts b/packages/mcp/src/demo/filesystem.ts new file mode 100644 index 000000000..4f3a21411 --- /dev/null +++ b/packages/mcp/src/demo/filesystem.ts @@ -0,0 +1,211 @@ +import express from 'express'; +import { EventSource } from 'eventsource'; +import { MCPConnection } from '../connection'; +import type { MCPOptions } from '../types/mcp'; + +// Set up EventSource for Node environment +global.EventSource = EventSource; + +const app = express(); +app.use(express.json()); + +let mcp: MCPConnection; + +const initializeMCP = async () => { + console.log('Initializing MCP with SSE transport...'); + + const mcpOptions: MCPOptions = { + // type: 'sse' as const, + // url: 'http://localhost:3001/sse', + type: 'stdio' as const, + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '/home/danny/LibreChat/'], + }; + + try { + // Clean up any existing instance + await MCPConnection.destroyInstance(); + + // Get singleton instance + mcp = MCPConnection.getInstance('filesystem', mcpOptions); + + // Add event listeners + mcp.on('connectionChange', (state) => { + console.log(`MCP connection state changed to: ${state}`); + }); + + mcp.on('error', (error) => { + console.error('MCP error:', error); + }); + + // Connect to server + console.log('Connecting to MCP server...'); + await mcp.connectClient(); + console.log('Connected to MCP server'); + } catch (error) { + console.error('Failed to connect to MCP server:', error); + } +}; + +// Initialize MCP connection +initializeMCP(); + +// API Endpoints +app.get('/status', (req, res) => { + res.json({ + connected: mcp.isConnected(), + state: mcp.getConnectionState(), + error: mcp.getLastError()?.message, + }); +}); + +app.get('/resources', async (req, res) => { + try { + const resources = await mcp.fetchResources(); + res.json({ resources }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +app.get('/tools', async (req, res) => { + try { + const tools = await mcp.fetchTools(); + res.json({ tools }); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// File operations +// @ts-ignore +app.get('/files/read', async (req, res) => { + const filePath = req.query.path as string; + if (!filePath) { + return res.status(400).json({ error: 'Path parameter is required' }); + } + + try { + const result = await mcp.client.callTool({ + name: 'read_file', + arguments: { path: filePath }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// @ts-ignore +app.post('/files/write', async (req, res) => { + const { path, content } = req.body; + if (!path || content === undefined) { + return res.status(400).json({ error: 'Path and content are required' }); + } + + try { + const result = await mcp.client.callTool({ + name: 'write_file', + arguments: { path, content }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// @ts-ignore +app.post('/files/edit', async (req, res) => { + const { path, edits, dryRun = false } = req.body; + if (!path || !edits) { + return res.status(400).json({ error: 'Path and edits are required' }); + } + + try { + const result = await mcp.client.callTool({ + name: 'edit_file', + arguments: { path, edits, dryRun }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Directory operations +// @ts-ignore +app.get('/directory/list', async (req, res) => { + const dirPath = req.query.path as string; + if (!dirPath) { + return res.status(400).json({ error: 'Path parameter is required' }); + } + + try { + const result = await mcp.client.callTool({ + name: 'list_directory', + arguments: { path: dirPath }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// @ts-ignore +app.post('/directory/create', async (req, res) => { + const { path } = req.body; + if (!path) { + return res.status(400).json({ error: 'Path is required' }); + } + + try { + const result = await mcp.client.callTool({ + name: 'create_directory', + arguments: { path }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Search endpoint +// @ts-ignore +app.get('/search', async (req, res) => { + const { path, pattern } = req.query; + if (!path || !pattern) { + return res.status(400).json({ error: 'Path and pattern parameters are required' }); + } + + try { + const result = await mcp.client.callTool({ + name: 'search_files', + arguments: { path, pattern }, + }); + res.json(result); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}); + +// Error handling +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Unhandled error:', err); + res.status(500).json({ + error: 'Internal server error', + message: err.message, + }); +}); + +// Cleanup on shutdown +process.on('SIGINT', async () => { + console.log('Shutting down...'); + await MCPConnection.destroyInstance(); + process.exit(0); +}); + +// Start server +const PORT = process.env.MCP_PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +}); diff --git a/packages/mcp/src/demo/servers.ts b/packages/mcp/src/demo/servers.ts new file mode 100644 index 000000000..1c5cad926 --- /dev/null +++ b/packages/mcp/src/demo/servers.ts @@ -0,0 +1,226 @@ +// server.ts +import express from 'express'; +import { EventSource } from 'eventsource'; +import { MCPManager } from '../manager'; +import { MCPConnection } from '../connection'; +import type * as t from '../types/mcp'; + +// Set up EventSource for Node environment +global.EventSource = EventSource; + +const app = express(); +app.use(express.json()); + +const mcpManager = MCPManager.getInstance(); + +const mcpServers: t.MCPServers = { + everything: { + type: 'sse' as const, + url: 'http://localhost:3001/sse', + }, + filesystem: { + type: 'stdio' as const, + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '/home/danny/LibreChat/'], + }, +}; + +// Generic helper to get connection and handle errors +const withConnection = async ( + serverName: string, + res: express.Response, + callback: (connection: MCPConnection) => Promise, +) => { + const connection = mcpManager.getConnection(serverName); + if (!connection) { + return res.status(404).json({ error: `Server "${serverName}" not found` }); + } + try { + await callback(connection); + } catch (error) { + res.status(500).json({ error: String(error) }); + } +}; + +// Common endpoints for all servers +// @ts-ignore +app.get('/status/:server', (req, res) => { + const connection = mcpManager.getConnection(req.params.server); + if (!connection) { + return res.status(404).json({ error: 'Server not found' }); + } + + res.json({ + connected: connection.isConnected(), + state: connection.getConnectionState(), + error: connection.getLastError()?.message, + }); +}); + +app.get('/resources/:server', async (req, res) => { + await withConnection(req.params.server, res, async (connection) => { + const resources = await connection.fetchResources(); + res.json({ resources }); + }); +}); + +app.get('/tools/:server', async (req, res) => { + await withConnection(req.params.server, res, async (connection) => { + const tools = await connection.fetchTools(); + res.json({ tools }); + }); +}); + +// "Everything" server specific endpoints +app.post('/everything/tools/echo', async (req, res) => { + await withConnection('everything', res, async (connection) => { + const { message } = req.body; + const result = await connection.client.callTool({ + name: 'echo', + arguments: { message }, + }); + res.json(result); + }); +}); + +app.post('/everything/tools/add', async (req, res) => { + await withConnection('everything', res, async (connection) => { + const { a, b } = req.body; + const result = await connection.client.callTool({ + name: 'add', + arguments: { a, b }, + }); + res.json(result); + }); +}); + +app.post('/everything/tools/long-operation', async (req, res) => { + await withConnection('everything', res, async (connection) => { + const { duration, steps } = req.body; + const result = await connection.client.callTool({ + name: 'longRunningOperation', + arguments: { duration, steps }, + }); + res.json(result); + }); +}); + +// Filesystem server specific endpoints +// @ts-ignore +app.get('/filesystem/files/read', async (req, res) => { + const filePath = req.query.path as string; + if (!filePath) { + return res.status(400).json({ error: 'Path parameter is required' }); + } + + await withConnection('filesystem', res, async (connection) => { + const result = await connection.client.callTool({ + name: 'read_file', + arguments: { path: filePath }, + }); + res.json(result); + }); +}); + +// @ts-ignore +app.post('/filesystem/files/write', async (req, res) => { + const { path, content } = req.body; + if (!path || content === undefined) { + return res.status(400).json({ error: 'Path and content are required' }); + } + + await withConnection('filesystem', res, async (connection) => { + const result = await connection.client.callTool({ + name: 'write_file', + arguments: { path, content }, + }); + res.json(result); + }); +}); + +// @ts-ignore +app.post('/filesystem/files/edit', async (req, res) => { + const { path, edits, dryRun = false } = req.body; + if (!path || !edits) { + return res.status(400).json({ error: 'Path and edits are required' }); + } + + await withConnection('filesystem', res, async (connection) => { + const result = await connection.client.callTool({ + name: 'edit_file', + arguments: { path, edits, dryRun }, + }); + res.json(result); + }); +}); + +// @ts-ignore +app.get('/filesystem/directory/list', async (req, res) => { + const dirPath = req.query.path as string; + if (!dirPath) { + return res.status(400).json({ error: 'Path parameter is required' }); + } + + await withConnection('filesystem', res, async (connection) => { + const result = await connection.client.callTool({ + name: 'list_directory', + arguments: { path: dirPath }, + }); + res.json(result); + }); +}); + +// @ts-ignore +app.post('/filesystem/directory/create', async (req, res) => { + const { path } = req.body; + if (!path) { + return res.status(400).json({ error: 'Path is required' }); + } + + await withConnection('filesystem', res, async (connection) => { + const result = await connection.client.callTool({ + name: 'create_directory', + arguments: { path }, + }); + res.json(result); + }); +}); + +// @ts-ignore +app.get('/filesystem/search', async (req, res) => { + const { path, pattern } = req.query; + if (!path || !pattern) { + return res.status(400).json({ error: 'Path and pattern parameters are required' }); + } + + await withConnection('filesystem', res, async (connection) => { + const result = await connection.client.callTool({ + name: 'search_files', + arguments: { path, pattern }, + }); + res.json(result); + }); +}); + +// Error handling +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Unhandled error:', err); + res.status(500).json({ + error: 'Internal server error', + message: err.message, + }); +}); + +// Cleanup on shutdown +process.on('SIGINT', async () => { + console.log('Shutting down...'); + await MCPManager.destroyInstance(); + process.exit(0); +}); + +// Start server +const PORT = process.env.MCP_PORT ?? 3000; +app.listen(PORT, async () => { + console.log(`Server running on http://localhost:${PORT}`); + await mcpManager.initializeMCP(mcpServers); +}); diff --git a/packages/mcp/src/enum.ts b/packages/mcp/src/enum.ts new file mode 100644 index 000000000..995ddfb52 --- /dev/null +++ b/packages/mcp/src/enum.ts @@ -0,0 +1,3 @@ +export enum CONSTANTS { + mcp_delimiter = '_mcp_', +} diff --git a/packages/mcp/src/examples/everything/everything.ts b/packages/mcp/src/examples/everything/everything.ts new file mode 100644 index 000000000..6080ff4ff --- /dev/null +++ b/packages/mcp/src/examples/everything/everything.ts @@ -0,0 +1,426 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + CallToolRequestSchema, + CreateMessageRequest, + CreateMessageResultSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, + Resource, + SetLevelRequestSchema, + SubscribeRequestSchema, + Tool, + ToolSchema, + UnsubscribeRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +const ToolInputSchema = ToolSchema.shape.inputSchema; +type ToolInput = z.infer; + +/* Input schemas for tools implemented in this server */ +const EchoSchema = z.object({ + message: z.string().describe('Message to echo'), +}); + +const AddSchema = z.object({ + a: z.number().describe('First number'), + b: z.number().describe('Second number'), +}); + +const LongRunningOperationSchema = z.object({ + duration: z.number().default(10).describe('Duration of the operation in seconds'), + steps: z.number().default(5).describe('Number of steps in the operation'), +}); + +const SampleLLMSchema = z.object({ + prompt: z.string().describe('The prompt to send to the LLM'), + maxTokens: z.number().default(100).describe('Maximum number of tokens to generate'), +}); + +const GetTinyImageSchema = z.object({}); + +enum ToolName { + ECHO = 'echo', + ADD = 'add', + LONG_RUNNING_OPERATION = 'longRunningOperation', + SAMPLE_LLM = 'sampleLLM', + GET_TINY_IMAGE = 'getTinyImage', +} + +enum PromptName { + SIMPLE = 'simple_prompt', + COMPLEX = 'complex_prompt', +} + +export const createServer = () => { + const server = new Server( + { + name: 'example-servers/everything', + version: '1.0.0', + }, + { + capabilities: { + prompts: {}, + resources: { subscribe: true }, + tools: {}, + logging: {}, + }, + }, + ); + + const subscriptions: Set = new Set(); + let updateInterval: NodeJS.Timeout | undefined; + + // Set up update interval for subscribed resources + // eslint-disable-next-line prefer-const + updateInterval = setInterval(() => { + // @ts-ignore + for (const uri of subscriptions) { + server.notification({ + method: 'notifications/resources/updated', + params: { uri }, + }); + } + }, 5000); + + // Helper method to request sampling from client + const requestSampling = async (context: string, uri: string, maxTokens = 100) => { + const request: CreateMessageRequest = { + method: 'sampling/createMessage', + params: { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Resource ${uri} context: ${context}`, + }, + }, + ], + systemPrompt: 'You are a helpful test server.', + maxTokens, + temperature: 0.7, + includeContext: 'thisServer', + }, + }; + + return await server.request(request, CreateMessageResultSchema); + }; + + const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => { + const uri = `test://static/resource/${i + 1}`; + if (i % 2 === 0) { + return { + uri, + name: `Resource ${i + 1}`, + mimeType: 'text/plain', + text: `Resource ${i + 1}: This is a plaintext resource`, + }; + } else { + const buffer = Buffer.from(`Resource ${i + 1}: This is a base64 blob`); + return { + uri, + name: `Resource ${i + 1}`, + mimeType: 'application/octet-stream', + blob: buffer.toString('base64'), + }; + } + }); + + const PAGE_SIZE = 10; + + server.setRequestHandler(ListResourcesRequestSchema, async (request) => { + const cursor = request.params?.cursor; + let startIndex = 0; + + if (cursor) { + const decodedCursor = parseInt(atob(cursor), 10); + if (!isNaN(decodedCursor)) { + startIndex = decodedCursor; + } + } + + const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_RESOURCES.length); + const resources = ALL_RESOURCES.slice(startIndex, endIndex); + + let nextCursor: string | undefined; + if (endIndex < ALL_RESOURCES.length) { + nextCursor = btoa(endIndex.toString()); + } + + return { + resources, + nextCursor, + }; + }); + + server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { + return { + resourceTemplates: [ + { + uriTemplate: 'test://static/resource/{id}', + name: 'Static Resource', + description: 'A static resource with a numeric ID', + }, + ], + }; + }); + + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + + if (uri.startsWith('test://static/resource/')) { + const index = parseInt(uri.split('/').pop() ?? '', 10) - 1; + if (index >= 0 && index < ALL_RESOURCES.length) { + const resource = ALL_RESOURCES[index]; + return { + contents: [resource], + }; + } + } + + throw new Error(`Unknown resource: ${uri}`); + }); + + server.setRequestHandler(SubscribeRequestSchema, async (request) => { + const { uri } = request.params; + subscriptions.add(uri); + + // Request sampling from client when someone subscribes + await requestSampling('A new subscription was started', uri); + return {}; + }); + + server.setRequestHandler(UnsubscribeRequestSchema, async (request) => { + subscriptions.delete(request.params.uri); + return {}; + }); + + server.setRequestHandler(ListPromptsRequestSchema, async () => { + return { + prompts: [ + { + name: PromptName.SIMPLE, + description: 'A prompt without arguments', + }, + { + name: PromptName.COMPLEX, + description: 'A prompt with arguments', + arguments: [ + { + name: 'temperature', + description: 'Temperature setting', + required: true, + }, + { + name: 'style', + description: 'Output style', + required: false, + }, + ], + }, + ], + }; + }); + + server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === PromptName.SIMPLE) { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'This is a simple prompt without arguments.', + }, + }, + ], + }; + } + + if (name === PromptName.COMPLEX) { + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `This is a complex prompt with arguments: temperature=${args?.temperature}, style=${args?.style}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: 'I understand. You\'ve provided a complex prompt with temperature and style arguments. How would you like me to proceed?', + }, + }, + { + role: 'user', + content: { + type: 'image', + data: MCP_TINY_IMAGE, + mimeType: 'image/png', + }, + }, + ], + }; + } + + throw new Error(`Unknown prompt: ${name}`); + }); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + const tools: Tool[] = [ + { + name: ToolName.ECHO, + description: 'Echoes back the input', + inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, + }, + { + name: ToolName.ADD, + description: 'Adds two numbers', + inputSchema: zodToJsonSchema(AddSchema) as ToolInput, + }, + { + name: ToolName.LONG_RUNNING_OPERATION, + description: 'Demonstrates a long running operation with progress updates', + inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput, + }, + { + name: ToolName.SAMPLE_LLM, + description: 'Samples from an LLM using MCP\'s sampling feature', + inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput, + }, + { + name: ToolName.GET_TINY_IMAGE, + description: 'Returns the MCP_TINY_IMAGE', + inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput, + }, + ]; + + return { tools }; + }); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === ToolName.ECHO) { + const validatedArgs = EchoSchema.parse(args); + return { + content: [{ type: 'text', text: `Echo: ${validatedArgs.message}` }], + }; + } + + if (name === ToolName.ADD) { + const validatedArgs = AddSchema.parse(args); + const sum = validatedArgs.a + validatedArgs.b; + return { + content: [ + { + type: 'text', + text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.`, + }, + ], + }; + } + + if (name === ToolName.LONG_RUNNING_OPERATION) { + const validatedArgs = LongRunningOperationSchema.parse(args); + const { duration, steps } = validatedArgs; + const stepDuration = duration / steps; + const progressToken = request.params._meta?.progressToken; + + for (let i = 1; i < steps + 1; i++) { + await new Promise((resolve) => setTimeout(resolve, stepDuration * 1000)); + + if (progressToken !== undefined) { + await server.notification({ + method: 'notifications/progress', + params: { + progress: i, + total: steps, + progressToken, + }, + }); + } + } + + return { + content: [ + { + type: 'text', + text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, + }, + ], + }; + } + + if (name === ToolName.SAMPLE_LLM) { + const validatedArgs = SampleLLMSchema.parse(args); + const { prompt, maxTokens } = validatedArgs; + + const result = await requestSampling(prompt, ToolName.SAMPLE_LLM, maxTokens); + return { + content: [{ type: 'text', text: `LLM sampling result: ${result}` }], + }; + } + + if (name === ToolName.GET_TINY_IMAGE) { + GetTinyImageSchema.parse(args); + return { + content: [ + { + type: 'text', + text: 'This is a tiny image:', + }, + { + type: 'image', + data: MCP_TINY_IMAGE, + mimeType: 'image/png', + }, + { + type: 'text', + text: 'The image above is the MCP tiny image.', + }, + ], + }; + } + + throw new Error(`Unknown tool: ${name}`); + }); + + server.setRequestHandler(SetLevelRequestSchema, async (request) => { + const { level } = request.params; + + // Demonstrate different log levels + await server.notification({ + method: 'notifications/message', + params: { + level: 'debug', + logger: 'test-server', + data: `Logging level set to: ${level}`, + }, + }); + + return {}; + }); + + const cleanup = async () => { + if (updateInterval) { + clearInterval(updateInterval); + } + }; + + return { server, cleanup }; +}; + +const MCP_TINY_IMAGE = + 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAKsGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgOfe9JDQEiIgJfQmSCeAlBBaAAXpYCMkAUKJMRBU7MriClZURLCs6KqIgo0idizYFsWC3QVZBNR1sWDDlXeBQ9jdd9575805c+a7c+efmf+e/z9nLgCdKZDJMlF1gCxpjjwyyI8dn5DIJvUABRiY0kBdIMyWcSMiwgCTUft3+dgGyJC9YzuU69/f/1fREImzhQBIBMbJomxhFsbHMe0TyuQ5ALg9mN9kbo5siK9gzJRjDWL8ZIhTR7hviJOHGY8fjomO5GGsDUCmCQTyVACaKeZn5wpTsTw0f4ztpSKJFGPsGbyzsmaLMMbqgiUWI8N4KD8n+S95Uv+WM1mZUyBIVfLIXoaF7C/JlmUK5v+fn+N/S1amYrSGOaa0NHlwJGaxvpAHGbNDlSxNnhI+yhLRcPwwpymCY0ZZmM1LHGWRwD9UuTZzStgop0gC+co8OfzoURZnB0SNsnx2pLJWipzHHWWBfKyuIiNG6U8T85X589Ki40Y5VxI7ZZSzM6JCx2J4Sr9cEansXywN8hurG6jce1b2X/Yr4SvX5qRFByv3LhjrXyzljuXMjlf2JhL7B4zFxCjjZTl+ylqyzAhlvDgzSOnPzo1Srs3BDuTY2gjlN0wXhESMMoRBELAhBjIhB+QggECQgBTEOeJ5Q2cUeLNl8+WS1LQcNhe7ZWI2Xyq0m8B2tHd0Bhi6syNH4j1r+C4irGtjvhWVAF4nBgcHT475Qm4BHEkCoNaO+SxnAKh3A1w5JVTIc0d8Q9cJCEAFNWCCDhiACViCLTiCK3iCLwRACIRDNCTATBBCGmRhnc+FhbAMCqAI1sNmKIOdsBv2wyE4CvVwCs7DZbgOt+AePIZ26IJX0AcfYQBBEBJCRxiIDmKImCE2iCPCQbyRACQMiUQSkCQkFZEiCmQhsgIpQoqRMmQXUokcQU4g55GrSCvyEOlAepF3yFcUh9JQJqqPmqMTUQ7KRUPRaHQGmorOQfPQfHQtWopWoAfROvQ8eh29h7ajr9B+HOBUcCycEc4Wx8HxcOG4RFwKTo5bjCvEleAqcNW4Rlwz7g6uHfca9wVPxDPwbLwt3hMfjI/BC/Fz8Ivxq/Fl+P34OvxF/B18B74P/51AJ+gRbAgeBD4hnpBKmEsoIJQQ9hJqCZcI9whdhI9EIpFFtCC6EYOJCcR04gLiauJ2Yg3xHLGV2EnsJ5FIOiQbkhcpnCQg5ZAKSFtJB0lnSbdJXaTPZBWyIdmRHEhOJEvJy8kl5APkM+Tb5G7yAEWdYkbxoIRTRJT5lHWUPZRGyk1KF2WAqkG1oHpRo6np1GXUUmo19RL1CfW9ioqKsYq7ylQVicpSlVKVwypXVDpUvtA0adY0Hm06TUFbS9tHO0d7SHtPp9PN6b70RHoOfS29kn6B/oz+WZWhaqfKVxWpLlEtV61Tva36Ro2iZqbGVZuplqdWonZM7abaa3WKurk6T12gvli9XP2E+n31fg2GhoNGuEaWxmqNAxpXNXo0SZrmmgGaIs18zd2aFzQ7GTiGCYPHEDJWMPYwLjG6mESmBZPPTGcWMQ8xW5h9WppazlqxWvO0yrVOa7WzcCxzFp+VyVrHOspqY30dpz+OO048btW46nG3x33SHq/tqy3WLtSu0b6n/VWHrROgk6GzQade56kuXtdad6ruXN0dupd0X49njvccLxxfOP7o+Ed6qJ61XqTeAr3dejf0+vUN9IP0Zfpb9S/ovzZgGfgapBtsMjhj0GvIMPQ2lBhuMjxr+JKtxeayM9ml7IvsPiM9o2AjhdEuoxajAWML4xjj5cY1xk9NqCYckxSTTSZNJn2mhqaTTReaVpk+MqOYcczSzLaYNZt9MrcwjzNfaV5v3mOhbcG3yLOosnhiSbf0sZxjWWF514poxbHKsNpudcsatXaxTrMut75pg9q42khsttu0TiBMcJ8gnVAx4b4tzZZrm2tbZdthx7ILs1tuV2/3ZqLpxMSJGyY2T/xu72Kfab/H/rGDpkOIw3KHRod3jtaOQsdyx7tOdKdApyVODU5vnW2cxc47nB+4MFwmu6x0aXL509XNVe5a7drrZuqW5LbN7T6HyYngrOZccSe4+7kvcT/l/sXD1SPH46jHH562nhmeBzx7JllMEk/aM6nTy9hL4LXLq92b7Z3k/ZN3u4+Rj8Cnwue5r4mvyHevbzfXipvOPch942fvJ/er9fvE8+At4p3zx/kH+Rf6twRoBsQElAU8CzQOTA2sCuwLcglaEHQumBAcGrwh+D5fny/kV/L7QtxCFoVcDKWFRoWWhT4Psw6ThzVORieHTN44+ckUsynSKfXhEM4P3xj+NMIiYk7EyanEqRFTy6e+iHSIXBjZHMWImhV1IOpjtF/0uujHMZYxipimWLXY6bGVsZ/i/OOK49rjJ8Yvir+eoJsgSWhIJCXGJu5N7J8WMG3ztK7pLtMLprfNsJgxb8bVmbozM2eenqU2SzDrWBIhKS7pQNI3QbigQtCfzE/eltwn5Am3CF+JfEWbRL1iL3GxuDvFK6U4pSfVK3Vjam+aT1pJ2msJT1ImeZsenL4z/VNGeMa+jMHMuMyaLHJWUtYJqaY0Q3pxtsHsebNbZTayAln7HI85m+f0yUPle7OR7BnZDTlMbDi6obBU/KDoyPXOLc/9PDd27rF5GvOk827Mt56/an53XmDezwvwC4QLmhYaLVy2sGMRd9Guxcji5MVNS0yW5C/pWhq0dP8y6rKMZb8st19evPzDirgVjfn6+UvzO38I+qGqQLVAXnB/pefKnT/if5T82LLKadXWVd8LRYXXiuyLSoq+rRauvrbGYU3pmsG1KWtb1rmu27GeuF66vm2Dz4b9xRrFecWdGydvrNvE3lS46cPmWZuvljiX7NxC3aLY0l4aVtqw1XTr+q3fytLK7pX7ldds09u2atun7aLtt3f47qjeqb+zaOfXnyQ/PdgVtKuuwryiZDdxd+7uF3ti9zT/zPm5cq/u3qK9f+6T7mvfH7n/YqVbZeUBvQPrqtAqRVXvwekHbx3yP9RQbVu9q4ZVU3QYDisOvzySdKTtaOjRpmOcY9XHzY5vq2XUFtYhdfPr+urT6tsbEhpaT4ScaGr0bKw9aXdy3ymjU+WntU6vO0M9k39m8Gze2f5zsnOvz6ee72ya1fT4QvyFuxenXmy5FHrpyuXAyxeauc1nr3hdOXXV4+qJa5xr9dddr9fdcLlR+4vLL7Utri11N91uNtzyv9XYOqn1zG2f2+fv+N+5fJd/9/q9Kfda22LaHtyffr/9gehBz8PMh28f5T4aeLz0CeFJ4VP1pyXP9J5V/Gr1a027a/vpDv+OG8+jnj/uFHa++i37t29d+S/oL0q6Dbsrexx7TvUG9t56Oe1l1yvZq4HXBb9r/L7tjeWb43/4/nGjL76v66387eC71e913u/74PyhqT+i/9nHrI8Dnwo/63ze/4Xzpflr3NfugbnfSN9K/7T6s/F76Pcng1mDgzKBXDA8CuAwRVNSAN7tA6AnADCwGYI6bWSmHhZk5D9gmOA/8cjcPSyuANWYGRqNeOcADmNqvhRAzRdgaCyK9gXUyUmpo/Pv8Kw+JAbYv8K0HECi2x6tebQU/iEjc/xf+v6nBWXWv9l/AV0EC6JTIblRAAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAAAAABAAAAkAAAAAEAAqACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAAAXNii1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB82lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KReh49gAAAjRJREFUOBGFlD2vMUEUx2clvoNCcW8hCqFAo1dKhEQpvsF9KrWEBh/ALbQ0KkInBI3SWyGPCCJEQliXgsTLefaca/bBWjvJzs6cOf/fnDkzOQJIjWm06/XKBEGgD8c6nU5VIWgBtQDPZPWtJE8O63a7LBgMMo/Hw0ql0jPjcY4RvmqXy4XMjUYDUwLtdhtmsxnYbDbI5/O0djqdFFKmsEiGZ9jP9gem0yn0ej2Yz+fg9XpfycimAD7DttstQTDKfr8Po9GIIg6Hw1Cr1RTgB+A72GAwgMPhQLBMJgNSXsFqtUI2myUo18pA6QJogefsPrLBX4QdCVatViklw+EQRFGEj88P2O12pEUGATmsXq+TaLPZ0AXgMRF2vMEqlQoJTSYTpNNpApvNZliv1/+BHDaZTAi2Wq1A3Ig0xmMej7+RcZjdbodUKkWAaDQK+GHjHPnImB88JrZIJAKFQgH2+z2BOczhcMiwRCIBgUAA+NN5BP6mj2DYff35gk6nA61WCzBn2JxO5wPM7/fLz4vD0E+OECfn8xl/0Gw2KbLxeAyLxQIsFgt8p75pDSO7h/HbpUWpewCike9WLpfB7XaDy+WCYrFI/slk8i0MnRRAUt46hPMI4vE4+Hw+ec7t9/44VgWigEeby+UgFArJWjUYOqhWG6x50rpcSfR6PVUfNOgEVRlTX0HhrZBKz4MZjUYWi8VoA+lc9H/VaRZYjBKrtXR8tlwumcFgeMWRbZpA9ORQWfVm8A/FsrLaxebd5wAAAABJRU5ErkJggg=='; diff --git a/packages/mcp/src/examples/everything/index.ts b/packages/mcp/src/examples/everything/index.ts new file mode 100644 index 000000000..4688e4bf8 --- /dev/null +++ b/packages/mcp/src/examples/everything/index.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createServer } from './everything'; + +async function main() { + const transport = new StdioServerTransport(); + const { server, cleanup } = createServer(); + + await server.connect(transport); + + // Cleanup on exit + process.on('SIGINT', async () => { + await cleanup(); + await server.close(); + process.exit(0); + }); +} + +main().catch((error) => { + console.error('Server error:', error); + process.exit(1); +}); diff --git a/packages/mcp/src/examples/everything/sse.ts b/packages/mcp/src/examples/everything/sse.ts new file mode 100644 index 000000000..38b329ae7 --- /dev/null +++ b/packages/mcp/src/examples/everything/sse.ts @@ -0,0 +1,24 @@ +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import express from 'express'; +import { createServer } from './everything.js'; +const app = express(); +const { server, cleanup } = createServer(); +let transport: SSEServerTransport; +app.get('/sse', async (req, res) => { + console.log('Received connection'); + transport = new SSEServerTransport('/message', res); + await server.connect(transport); + server.onclose = async () => { + await cleanup(); + await server.close(); + process.exit(0); + }; +}); +app.post('/message', async (req, res) => { + console.log('Received message'); + await transport.handlePostMessage(req, res); +}); +const PORT = process.env.SSE_PORT ?? 3001; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/packages/mcp/src/examples/filesystem.ts b/packages/mcp/src/examples/filesystem.ts new file mode 100644 index 000000000..c2e3c430f --- /dev/null +++ b/packages/mcp/src/examples/filesystem.ts @@ -0,0 +1,700 @@ +#!/usr/bin/env node +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { + JSONRPCMessage, + CallToolRequestSchema, + ListToolsRequestSchema, + InitializeRequestSchema, + ToolSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { diffLines, createTwoFilesPatch } from 'diff'; +import { IncomingMessage, ServerResponse } from 'node:http'; +import { minimatch } from 'minimatch'; +import express from 'express'; + +function normalizePath(p: string): string { + return path.normalize(p).toLowerCase(); +} + +function expandHome(filepath: string): string { + if (filepath.startsWith('~/') || filepath === '~') { + return path.join(os.homedir(), filepath.slice(1)); + } + return filepath; +} + +// Command line argument parsing +const args = process.argv.slice(2); + +// Parse command line arguments for transport type +const transportArg = args.find((arg) => arg.startsWith('--transport=')); +const portArg = args.find((arg) => arg.startsWith('--port=')); +const directories = args.filter((arg) => !arg.startsWith('--')); + +if (directories.length === 0) { + console.error( + 'Usage: mcp-server-filesystem [--transport=stdio|sse] [--port=3000] [additional-directories...]', + ); + process.exit(1); +} + +// Extract transport type and port from arguments +const transport = transportArg ? (transportArg.split('=')[1] as 'stdio' | 'sse') : 'stdio'; + +const port = portArg ? parseInt(portArg.split('=')[1], 10) : undefined; + +// Store allowed directories in normalized form +const allowedDirectories = directories.map((dir) => normalizePath(path.resolve(expandHome(dir)))); + +// Validate that all directories exist and are accessible +/** @ts-ignore */ +await Promise.all( + directories.map(async (dir) => { + try { + const stats = await fs.stat(dir); + if (!stats.isDirectory()) { + console.error(`Error: ${dir} is not a directory`); + process.exit(1); + } + } catch (error) { + console.error(`Error accessing directory ${dir}:`, error); + process.exit(1); + } + }), +); + +// Security utilities +async function validatePath(requestedPath: string): Promise { + const expandedPath = expandHome(requestedPath); + const absolute = path.isAbsolute(expandedPath) + ? path.resolve(expandedPath) + : path.resolve(process.cwd(), expandedPath); + + const normalizedRequested = normalizePath(absolute); + + // Check if path is within allowed directories + const isAllowed = allowedDirectories.some((dir) => normalizedRequested.startsWith(dir)); + if (!isAllowed) { + throw new Error( + `Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join( + ', ', + )}`, + ); + } + + // Handle symlinks by checking their real path + try { + const realPath = await fs.realpath(absolute); + const normalizedReal = normalizePath(realPath); + const isRealPathAllowed = allowedDirectories.some((dir) => normalizedReal.startsWith(dir)); + if (!isRealPathAllowed) { + throw new Error('Access denied - symlink target outside allowed directories'); + } + return realPath; + } catch (error) { + // For new files that don't exist yet, verify parent directory + const parentDir = path.dirname(absolute); + try { + const realParentPath = await fs.realpath(parentDir); + const normalizedParent = normalizePath(realParentPath); + const isParentAllowed = allowedDirectories.some((dir) => normalizedParent.startsWith(dir)); + if (!isParentAllowed) { + throw new Error('Access denied - parent directory outside allowed directories'); + } + return absolute; + } catch { + throw new Error(`Parent directory does not exist: ${parentDir}`); + } + } +} + +// Schema definitions +const ReadFileArgsSchema = z.object({ + path: z.string(), +}); + +const ReadMultipleFilesArgsSchema = z.object({ + paths: z.array(z.string()), +}); + +const WriteFileArgsSchema = z.object({ + path: z.string(), + content: z.string(), +}); + +const EditOperation = z.object({ + oldText: z.string().describe('Text to search for - must match exactly'), + newText: z.string().describe('Text to replace with'), +}); + +const EditFileArgsSchema = z.object({ + path: z.string(), + edits: z.array(EditOperation), + dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format'), +}); + +const CreateDirectoryArgsSchema = z.object({ + path: z.string(), +}); + +const ListDirectoryArgsSchema = z.object({ + path: z.string(), +}); + +const MoveFileArgsSchema = z.object({ + source: z.string(), + destination: z.string(), +}); + +const SearchFilesArgsSchema = z.object({ + path: z.string(), + pattern: z.string(), + excludePatterns: z.array(z.string()).optional().default([]), +}); + +const GetFileInfoArgsSchema = z.object({ + path: z.string(), +}); + +const ToolInputSchema = ToolSchema.shape.inputSchema; +type ToolInput = z.infer; + +interface FileInfo { + size: number; + created: Date; + modified: Date; + accessed: Date; + isDirectory: boolean; + isFile: boolean; + permissions: string; +} + +// Server setup +const server = new Server( + { + name: 'secure-filesystem-server', + version: '0.2.0', + }, + { + capabilities: { + tools: {}, + }, + }, +); + +// Tool implementations +async function getFileStats(filePath: string): Promise { + const stats = await fs.stat(filePath); + return { + size: stats.size, + created: stats.birthtime, + modified: stats.mtime, + accessed: stats.atime, + isDirectory: stats.isDirectory(), + isFile: stats.isFile(), + permissions: stats.mode.toString(8).slice(-3), + }; +} + +async function searchFiles( + rootPath: string, + pattern: string, + excludePatterns: string[] = [], +): Promise { + const results: string[] = []; + + async function search(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + + try { + // Validate each path before processing + await validatePath(fullPath); + + // Check if path matches any exclude pattern + const relativePath = path.relative(rootPath, fullPath); + const shouldExclude = excludePatterns.some((pattern) => { + const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`; + return minimatch(relativePath, globPattern, { dot: true }); + }); + + if (shouldExclude) { + continue; + } + + if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { + results.push(fullPath); + } + + if (entry.isDirectory()) { + await search(fullPath); + } + } catch (error) { + // Skip invalid paths during search + continue; + } + } + } + + await search(rootPath); + return results; +} + +// file editing and diffing utilities +function normalizeLineEndings(text: string): string { + return text.replace(/\r\n/g, '\n'); +} + +function createUnifiedDiff(originalContent: string, newContent: string, filepath = 'file'): string { + // Ensure consistent line endings for diff + const normalizedOriginal = normalizeLineEndings(originalContent); + const normalizedNew = normalizeLineEndings(newContent); + + return createTwoFilesPatch( + filepath, + filepath, + normalizedOriginal, + normalizedNew, + 'original', + 'modified', + ); +} + +async function applyFileEdits( + filePath: string, + edits: Array<{ oldText: string; newText: string }>, + dryRun = false, +): Promise { + // Read file content and normalize line endings + const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); + + // Apply edits sequentially + let modifiedContent = content; + for (const edit of edits) { + const normalizedOld = normalizeLineEndings(edit.oldText); + const normalizedNew = normalizeLineEndings(edit.newText); + + // If exact match exists, use it + if (modifiedContent.includes(normalizedOld)) { + modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); + continue; + } + + // Otherwise, try line-by-line matching with flexibility for whitespace + const oldLines = normalizedOld.split('\n'); + const contentLines = modifiedContent.split('\n'); + let matchFound = false; + + for (let i = 0; i <= contentLines.length - oldLines.length; i++) { + const potentialMatch = contentLines.slice(i, i + oldLines.length); + + // Compare lines with normalized whitespace + const isMatch = oldLines.every((oldLine, j) => { + const contentLine = potentialMatch[j]; + return oldLine.trim() === contentLine.trim(); + }); + + if (isMatch) { + // Preserve original indentation of first line + const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; + const newLines = normalizedNew.split('\n').map((line, j) => { + if (j === 0) { + return originalIndent + line.trimStart(); + } + // For subsequent lines, try to preserve relative indentation + const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''; + const newIndent = line.match(/^\s*/)?.[0] || ''; + if (oldIndent && newIndent) { + const relativeIndent = newIndent.length - oldIndent.length; + return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart(); + } + return line; + }); + + contentLines.splice(i, oldLines.length, ...newLines); + modifiedContent = contentLines.join('\n'); + matchFound = true; + break; + } + } + + if (!matchFound) { + throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); + } + } + + // Create unified diff + const diff = createUnifiedDiff(content, modifiedContent, filePath); + + // Format diff with appropriate number of backticks + let numBackticks = 3; + while (diff.includes('`'.repeat(numBackticks))) { + numBackticks++; + } + const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; + + if (!dryRun) { + await fs.writeFile(filePath, modifiedContent, 'utf-8'); + } + + return formattedDiff; +} + +// Tool handlers +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'read_file', + description: + 'Read the complete contents of a file from the file system. ' + + 'Handles various text encodings and provides detailed error messages ' + + 'if the file cannot be read. Use this tool when you need to examine ' + + 'the contents of a single file. Only works within allowed directories.', + inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput, + }, + { + name: 'read_multiple_files', + description: + 'Read the contents of multiple files simultaneously. This is more ' + + 'efficient than reading files one by one when you need to analyze ' + + 'or compare multiple files. Each file\'s content is returned with its ' + + 'path as a reference. Failed reads for individual files won\'t stop ' + + 'the entire operation. Only works within allowed directories.', + inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput, + }, + { + name: 'write_file', + description: + 'Create a new file or completely overwrite an existing file with new content. ' + + 'Use with caution as it will overwrite existing files without warning. ' + + 'Handles text content with proper encoding. Only works within allowed directories.', + inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput, + }, + { + name: 'edit_file', + description: + 'Make line-based edits to a text file. Each edit replaces exact line sequences ' + + 'with new content. Returns a git-style diff showing the changes made. ' + + 'Only works within allowed directories.', + inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput, + }, + { + name: 'create_directory', + description: + 'Create a new directory or ensure a directory exists. Can create multiple ' + + 'nested directories in one operation. If the directory already exists, ' + + 'this operation will succeed silently. Perfect for setting up directory ' + + 'structures for projects or ensuring required paths exist. Only works within allowed directories.', + inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput, + }, + { + name: 'list_directory', + description: + 'Get a detailed listing of all files and directories in a specified path. ' + + 'Results clearly distinguish between files and directories with [FILE] and [DIR] ' + + 'prefixes. This tool is essential for understanding directory structure and ' + + 'finding specific files within a directory. Only works within allowed directories.', + inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, + }, + { + name: 'move_file', + description: + 'Move or rename files and directories. Can move files between directories ' + + 'and rename them in a single operation. If the destination exists, the ' + + 'operation will fail. Works across different directories and can be used ' + + 'for simple renaming within the same directory. Both source and destination must be within allowed directories.', + inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput, + }, + { + name: 'search_files', + description: + 'Recursively search for files and directories matching a pattern. ' + + 'Searches through all subdirectories from the starting path. The search ' + + 'is case-insensitive and matches partial names. Returns full paths to all ' + + 'matching items. Great for finding files when you don\'t know their exact location. ' + + 'Only searches within allowed directories.', + inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput, + }, + { + name: 'get_file_info', + description: + 'Retrieve detailed metadata about a file or directory. Returns comprehensive ' + + 'information including size, creation time, last modified time, permissions, ' + + 'and type. This tool is perfect for understanding file characteristics ' + + 'without reading the actual content. Only works within allowed directories.', + inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput, + }, + { + name: 'list_allowed_directories', + description: + 'Returns the list of directories that this server is allowed to access. ' + + 'Use this to understand which directories are available before trying to access files.', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + }, + ], + }; +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + const { name, arguments: args } = request.params; + + switch (name) { + case 'read_file': { + const parsed = ReadFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const content = await fs.readFile(validPath, 'utf-8'); + return { + content: [{ type: 'text', text: content }], + }; + } + + case 'read_multiple_files': { + const parsed = ReadMultipleFilesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`); + } + const results = await Promise.all( + parsed.data.paths.map(async (filePath: string) => { + try { + const validPath = await validatePath(filePath); + const content = await fs.readFile(validPath, 'utf-8'); + return `${filePath}:\n${content}\n`; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return `${filePath}: Error - ${errorMessage}`; + } + }), + ); + return { + content: [{ type: 'text', text: results.join('\n---\n') }], + }; + } + + case 'write_file': { + const parsed = WriteFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for write_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + await fs.writeFile(validPath, parsed.data.content, 'utf-8'); + return { + content: [{ type: 'text', text: `Successfully wrote to ${parsed.data.path}` }], + }; + } + + case 'edit_file': { + const parsed = EditFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for edit_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun); + return { + content: [{ type: 'text', text: result }], + }; + } + + case 'create_directory': { + const parsed = CreateDirectoryArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for create_directory: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + await fs.mkdir(validPath, { recursive: true }); + return { + content: [{ type: 'text', text: `Successfully created directory ${parsed.data.path}` }], + }; + } + + case 'list_directory': { + const parsed = ListDirectoryArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for list_directory: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + const formatted = entries + .map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`) + .join('\n'); + return { + content: [{ type: 'text', text: formatted }], + }; + } + + case 'move_file': { + const parsed = MoveFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for move_file: ${parsed.error}`); + } + const validSourcePath = await validatePath(parsed.data.source); + const validDestPath = await validatePath(parsed.data.destination); + await fs.rename(validSourcePath, validDestPath); + return { + content: [ + { + type: 'text', + text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}`, + }, + ], + }; + } + + case 'search_files': { + const parsed = SearchFilesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for search_files: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const results = await searchFiles( + validPath, + parsed.data.pattern, + parsed.data.excludePatterns, + ); + return { + content: [ + { type: 'text', text: results.length > 0 ? results.join('\n') : 'No matches found' }, + ], + }; + } + + case 'get_file_info': { + const parsed = GetFileInfoArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const info = await getFileStats(validPath); + return { + content: [ + { + type: 'text', + text: Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'), + }, + ], + }; + } + + case 'list_allowed_directories': { + return { + content: [ + { + type: 'text', + text: `Allowed directories:\n${allowedDirectories.join('\n')}`, + }, + ], + }; + } + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + content: [{ type: 'text', text: `Error: ${errorMessage}` }], + isError: true, + }; + } +}); + +// Start server +// async function runServer() { +// const transport = new StdioServerTransport(); +// await server.connect(transport); +// console.error('Secure MCP Filesystem Server running on stdio'); +// console.error('Allowed directories:', allowedDirectories); +// } + +// runServer().catch((error) => { +// console.error('Fatal error running server:', error); +// process.exit(1); +// }); + +async function runServer(transport: 'stdio' | 'sse', port?: number) { + if (transport === 'stdio') { + const stdioTransport = new StdioServerTransport(); + await server.connect(stdioTransport); + console.error('Secure MCP Filesystem Server running on stdio'); + console.error('Allowed directories:', allowedDirectories); + } else { + const app = express(); + app.use(express.json()); + + // Set up CORS + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') { + return res.sendStatus(200); + } + next(); + }); + + let transport: SSEServerTransport; + + // SSE endpoint + app.get('/sse', async (req, res) => { + console.log('New SSE connection'); + transport = new SSEServerTransport('/message', res); + await server.connect(transport); + + // Cleanup on close + res.on('close', async () => { + console.log('SSE connection closed'); + await server.close(); + }); + }); + + // Message endpoint + app.post('/message', async (req, res) => { + if (!transport) { + return res.status(503).send('SSE connection not established'); + } + await transport.handlePostMessage(req, res); + }); + + const serverPort = port || 3001; + app.listen(serverPort, () => { + console.log( + `Secure MCP Filesystem Server running on SSE at http://localhost:${serverPort}/sse`, + ); + console.log('Allowed directories:', allowedDirectories); + }); + } +} + +if (directories.length === 0) { + console.error( + 'Usage: mcp-server-filesystem [--transport=stdio|sse] [--port=3000] [additional-directories...]', + ); + process.exit(1); +} + +// Start the server with the specified transport +runServer(transport, port).catch((error) => { + console.error('Fatal error running server:', error); + process.exit(1); +}); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 000000000..ecf601787 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,4 @@ +/* MCP */ +export * from './manager'; +/* types */ +export type * from './types/mcp'; diff --git a/packages/mcp/src/manager.ts b/packages/mcp/src/manager.ts new file mode 100644 index 000000000..bf74478b1 --- /dev/null +++ b/packages/mcp/src/manager.ts @@ -0,0 +1,238 @@ +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import type { JsonSchemaType } from 'librechat-data-provider'; +import type { Logger } from 'winston'; +import type * as t from './types/mcp'; +import { formatToolContent } from './parsers'; +import { MCPConnection } from './connection'; +import { CONSTANTS } from './enum'; + +export class MCPManager { + private static instance: MCPManager | null = null; + private connections: Map = new Map(); + private logger: Logger; + + private static getDefaultLogger(): Logger { + return { + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, + } as Logger; + } + + private constructor(logger?: Logger) { + this.logger = logger || MCPManager.getDefaultLogger(); + } + + public static getInstance(logger?: Logger): MCPManager { + if (!MCPManager.instance) { + MCPManager.instance = new MCPManager(logger); + } + return MCPManager.instance; + } + + public async initializeMCP(mcpServers: t.MCPServers): Promise { + this.logger.info('[MCP] Initializing servers'); + + const entries = Object.entries(mcpServers); + const initializedServers = new Set(); + const connectionResults = await Promise.allSettled( + entries.map(async ([serverName, config], i) => { + const connection = new MCPConnection(serverName, config, this.logger); + + connection.on('connectionChange', (state) => { + this.logger.info(`[MCP][${serverName}] Connection state: ${state}`); + }); + + try { + const connectionTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Connection timeout')), 30000), + ); + + const connectionAttempt = this.initializeServer(connection, serverName); + await Promise.race([connectionAttempt, connectionTimeout]); + + if (connection.isConnected()) { + initializedServers.add(i); + this.connections.set(serverName, connection); + + const serverCapabilities = connection.client.getServerCapabilities(); + this.logger.info( + `[MCP][${serverName}] Capabilities: ${JSON.stringify(serverCapabilities)}`, + ); + + if (serverCapabilities?.tools) { + const tools = await connection.client.listTools(); + if (tools.tools.length) { + this.logger.info( + `[MCP][${serverName}] Available tools: ${tools.tools + .map((tool) => tool.name) + .join(', ')}`, + ); + } + } + } + } catch (error) { + this.logger.error(`[MCP][${serverName}] Initialization failed`, error); + throw error; + } + }), + ); + + const failedConnections = connectionResults.filter( + (result): result is PromiseRejectedResult => result.status === 'rejected', + ); + + this.logger.info(`[MCP] Initialized ${initializedServers.size}/${entries.length} server(s)`); + + if (failedConnections.length > 0) { + this.logger.warn( + `[MCP] ${failedConnections.length}/${entries.length} server(s) failed to initialize`, + ); + } + + entries.forEach(([serverName], index) => { + if (initializedServers.has(index)) { + this.logger.info(`[MCP][${serverName}] ✓ Initialized`); + } else { + this.logger.info(`[MCP][${serverName}] ✗ Failed`); + } + }); + + if (initializedServers.size === entries.length) { + this.logger.info('[MCP] All servers initialized successfully'); + } else if (initializedServers.size === 0) { + this.logger.error('[MCP] No servers initialized'); + } + } + + private async initializeServer(connection: MCPConnection, serverName: string): Promise { + const maxAttempts = 3; + let attempts = 0; + + while (attempts < maxAttempts) { + try { + await connection.connect(); + + if (connection.isConnected()) { + return; + } + } catch (error) { + attempts++; + + if (attempts === maxAttempts) { + this.logger.error(`[MCP][${serverName}] Failed after ${maxAttempts} attempts`); + throw error; + } + + await new Promise((resolve) => setTimeout(resolve, 2000 * attempts)); + } + } + } + + public getConnection(serverName: string): MCPConnection | undefined { + return this.connections.get(serverName); + } + + public getAllConnections(): Map { + return this.connections; + } + + public async mapAvailableTools(availableTools: t.LCAvailableTools): Promise { + for (const [serverName, connection] of this.connections.entries()) { + try { + if (connection.isConnected() !== true) { + this.logger.warn(`Connection ${serverName} is not connected. Skipping tool fetch.`); + continue; + } + + const tools = await connection.fetchTools(); + for (const tool of tools) { + const name = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`; + availableTools[name] = { + type: 'function', + ['function']: { + name, + description: tool.description, + parameters: tool.inputSchema as JsonSchemaType, + }, + }; + } + } catch (error) { + this.logger.warn(`[MCP][${serverName}] Not connected, skipping tool fetch`); + } + } + } + + public async loadManifestTools(manifestTools: t.LCToolManifest): Promise { + for (const [serverName, connection] of this.connections.entries()) { + try { + if (connection.isConnected() !== true) { + this.logger.warn(`Connection ${serverName} is not connected. Skipping tool fetch.`); + continue; + } + + const tools = await connection.fetchTools(); + for (const tool of tools) { + const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`; + manifestTools.push({ + name: tool.name, + pluginKey, + description: tool.description ?? '', + icon: connection.iconPath, + }); + } + } catch (error) { + this.logger.error(`[MCP][${serverName}] Error fetching tools`, error); + } + } + } + + async callTool( + serverName: string, + toolName: string, + provider: t.Provider, + toolArguments?: Record, + ): Promise { + const connection = this.connections.get(serverName); + if (!connection) { + throw new Error( + `No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`, + ); + } + const result = await connection.client.request( + { + method: 'tools/call', + params: { + name: toolName, + arguments: toolArguments, + }, + }, + CallToolResultSchema, + ); + return formatToolContent(result, provider); + } + + public async disconnectServer(serverName: string): Promise { + const connection = this.connections.get(serverName); + if (connection) { + await connection.disconnect(); + this.connections.delete(serverName); + } + } + + public async disconnectAll(): Promise { + const disconnectPromises = Array.from(this.connections.values()).map((connection) => + connection.disconnect(), + ); + await Promise.all(disconnectPromises); + this.connections.clear(); + } + + public static async destroyInstance(): Promise { + if (MCPManager.instance) { + await MCPManager.instance.disconnectAll(); + MCPManager.instance = null; + } + } +} diff --git a/packages/mcp/src/parsers.ts b/packages/mcp/src/parsers.ts new file mode 100644 index 000000000..2f1803b91 --- /dev/null +++ b/packages/mcp/src/parsers.ts @@ -0,0 +1,157 @@ +import type * as t from './types/mcp'; +const RECOGNIZED_PROVIDERS = new Set(['google', 'anthropic', 'openAI']); + +const imageFormatters: Record = { + // google: (item) => ({ + // type: 'image', + // inlineData: { + // mimeType: item.mimeType, + // data: item.data, + // }, + // }), + // anthropic: (item) => ({ + // type: 'image', + // source: { + // type: 'base64', + // media_type: item.mimeType, + // data: item.data, + // }, + // }), + default: (item) => ({ + type: 'image_url', + image_url: { + url: item.data.startsWith('http') ? item.data : `data:${item.mimeType};base64,${item.data}`, + }, + }), +}; + +function isImageContent(item: t.ToolContentPart): item is t.ImageContent { + return item.type === 'image'; +} + +function parseAsString(result: t.MCPToolCallResponse): string { + const content = result?.content ?? []; + if (!content.length) { + return '(No response)'; + } + + const text = content + .map((item) => { + if (item.type === 'text') { + return item.text; + } + if (item.type === 'resource') { + const resourceText = []; + if (item.resource.text != null && item.resource.text) { + resourceText.push(item.resource.text); + } + if (item.resource.uri) { + resourceText.push(`Resource URI: ${item.resource.uri}`); + } + if (item.resource.mimeType != null && item.resource.mimeType) { + resourceText.push(`Type: ${item.resource.mimeType}`); + } + return resourceText.join('\n'); + } + return JSON.stringify(item, null, 2); + }) + .filter(Boolean) + .join('\n\n'); + + return text; +} + +/** + * Converts MCPToolCallResponse content into recognized content block types + * Recognized types: "image", "image_url", "text", "json" + * + * @param {t.MCPToolCallResponse} result - The MCPToolCallResponse object + * @param {string} provider - The provider name (google, anthropic, openai) + * @returns {Array} Formatted content blocks + */ +/** + * Converts MCPToolCallResponse content into recognized content block types + * First element: string or formatted content (excluding image_url) + * Second element: image_url content if any + * + * @param {t.MCPToolCallResponse} result - The MCPToolCallResponse object + * @param {string} provider - The provider name (google, anthropic, openai) + * @returns {t.FormattedToolResponse} Tuple of content and image_urls + */ +export function formatToolContent( + result: t.MCPToolCallResponse, + provider: t.Provider, +): t.FormattedToolResponse { + if (!RECOGNIZED_PROVIDERS.has(provider)) { + return [parseAsString(result), undefined]; + } + + const content = result?.content ?? []; + if (!content.length) { + return [[{ type: 'text', text: '(No response)' }], undefined]; + } + + const formattedContent: t.FormattedContent[] = []; + const imageUrls: t.FormattedContent[] = []; + let currentTextBlock = ''; + + type ContentHandler = undefined | ((item: t.ToolContentPart) => void); + + const contentHandlers: { + text: (item: Extract) => void; + image: (item: t.ToolContentPart) => void; + resource: (item: Extract) => void; + } = { + text: (item) => { + currentTextBlock += (currentTextBlock ? '\n\n' : '') + item.text; + }, + + image: (item) => { + if (!isImageContent(item)) { + return; + } + if (currentTextBlock) { + formattedContent.push({ type: 'text', text: currentTextBlock }); + currentTextBlock = ''; + } + const formatter = imageFormatters.default as t.ImageFormatter; + const formattedImage = formatter(item); + + if (formattedImage.type === 'image_url') { + imageUrls.push(formattedImage); + } else { + formattedContent.push(formattedImage); + } + }, + + resource: (item) => { + const resourceText = []; + if (item.resource.text != null && item.resource.text) { + resourceText.push(item.resource.text); + } + if (item.resource.uri.length) { + resourceText.push(`Resource URI: ${item.resource.uri}`); + } + if (item.resource.mimeType != null && item.resource.mimeType) { + resourceText.push(`Type: ${item.resource.mimeType}`); + } + currentTextBlock += (currentTextBlock ? '\n\n' : '') + resourceText.join('\n'); + }, + }; + + for (const item of content) { + const handler = contentHandlers[item.type as keyof typeof contentHandlers] as ContentHandler; + if (handler) { + handler(item as never); + } else { + const stringified = JSON.stringify(item, null, 2); + currentTextBlock += (currentTextBlock ? '\n\n' : '') + stringified; + } + } + + if (currentTextBlock) { + formattedContent.push({ type: 'text', text: currentTextBlock }); + } + + return [formattedContent, imageUrls.length ? { content: imageUrls } : undefined]; +} diff --git a/packages/mcp/src/types/mcp.ts b/packages/mcp/src/types/mcp.ts new file mode 100644 index 000000000..106eb04e1 --- /dev/null +++ b/packages/mcp/src/types/mcp.ts @@ -0,0 +1,109 @@ +import { z } from 'zod'; +import { + SSEOptionsSchema, + MCPOptionsSchema, + MCPServersSchema, + StdioOptionsSchema, + WebSocketOptionsSchema, +} from 'librechat-data-provider'; +import type { JsonSchemaType, TPlugin } from 'librechat-data-provider'; +import { ToolSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; + +export type StdioOptions = z.infer; +export type WebSocketOptions = z.infer; +export type SSEOptions = z.infer; +export type MCPOptions = z.infer; +export type MCPServers = z.infer; +export interface MCPResource { + uri: string; + name: string; + description?: string; + mimeType?: string; +} +export interface LCTool { + name: string; + description?: string; + parameters: JsonSchemaType; +} + +export interface LCFunctionTool { + type: 'function'; + ['function']: LCTool; +} + +export type LCAvailableTools = Record; + +export type LCToolManifest = TPlugin[]; +export interface MCPPrompt { + name: string; + description?: string; + arguments?: Array<{ name: string }>; +} + +export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error'; + +export type MCPTool = z.infer; +export type MCPToolListResponse = z.infer; +export type ToolContentPart = + | { + type: 'text'; + text: string; + } + | { + type: 'image'; + data: string; + mimeType: string; + } + | { + type: 'resource'; + resource: { + uri: string; + mimeType?: string; + text?: string; + blob?: string; + }; + }; +export type ImageContent = Extract; +export type MCPToolCallResponse = + | undefined + | { + _meta?: Record; + content?: Array; + isError?: boolean; + }; + +export type Provider = 'google' | 'anthropic' | 'openAI'; + +export type FormattedContent = + | { + type: 'text'; + text: string; + } + | { + type: 'image'; + inlineData: { + mimeType: string; + data: string; + }; + } + | { + type: 'image'; + source: { + type: 'base64'; + media_type: string; + data: string; + }; + } + | { + type: 'image_url'; + image_url: { + url: string; + }; + }; + +export type ImageFormatter = (item: ImageContent) => FormattedContent; + +export type FormattedToolResponse = [ + string | FormattedContent[], + { content: FormattedContent[] } | undefined, +]; diff --git a/packages/mcp/tsconfig-paths-bootstrap.mjs b/packages/mcp/tsconfig-paths-bootstrap.mjs new file mode 100644 index 000000000..5b1c8bd16 --- /dev/null +++ b/packages/mcp/tsconfig-paths-bootstrap.mjs @@ -0,0 +1,23 @@ +import path from 'path'; +import { pathToFileURL } from 'url'; +// @ts-ignore +import { resolve as resolveTs } from 'ts-node/esm'; +import * as tsConfigPaths from 'tsconfig-paths'; + +// @ts-ignore +const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig( + path.resolve('./tsconfig.json'), // Updated path +); +const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths); + +export function resolve(specifier, context, defaultResolve) { + const match = matchPath(specifier); + if (match) { + return resolveTs(pathToFileURL(match).href, context, defaultResolve); + } + return resolveTs(specifier, context, defaultResolve); +} + +// @ts-ignore +export { load, getFormat, transformSource } from 'ts-node/esm'; +// node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ../../api/demo/everything.ts \ No newline at end of file diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 000000000..94667987f --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "./dist/types", + "module": "esnext", + "noImplicitAny": true, + "outDir": "./types", + "target": "es2015", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "lib": ["es2017", "dom", "ES2021.String"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "sourceMap": true, + "baseUrl": "." // This should be the root of your package + }, + "ts-node": { + "experimentalSpecifierResolution": "node", + "transpileOnly": true, + "esm": true + }, + "exclude": ["node_modules", "dist", "types"], + "include": ["src/**/*", "types/index.d.ts", "types/react-query/index.d.ts"] +} diff --git a/packages/mcp/tsconfig.spec.json b/packages/mcp/tsconfig.spec.json new file mode 100644 index 000000000..f766b118e --- /dev/null +++ b/packages/mcp/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "outDir": "./dist/tests", + "baseUrl": "." + }, + "include": ["specs/**/*", "src/**/*"], + "exclude": ["node_modules", "dist"] +}