Skip to content

Commit

Permalink
🔧 feat: Initial MCP Support (Tools) (#5015)
Browse files Browse the repository at this point in the history
* 📝 chore: Add comment to clarify purpose of check_updates.sh script

* feat: mcp package

* feat: add librechat-mcp package and update dependencies

* feat: refactor MCPConnectionSingleton to handle transport initialization and connection management

* feat: change private methods to public in MCPConnectionSingleton for improved accessibility

* feat: filesystem demo

* chore: everything demo and move everything under mcp workspace

* chore: move ts-node to mcp workspace

* feat: mcp examples

* feat: working sse MCP example

* refactor: rename MCPConnectionSingleton to MCPConnection for clarity

* refactor: replace MCPConnectionSingleton with MCPConnection for consistency

* refactor: manager/connections

* refactor: update MCPConnection to use type definitions from mcp types

* refactor: update MCPManager to use winston logger and enhance server initialization

* refactor: share logger between connections and manager

* refactor: add schema definitions and update MCPManager to accept logger parameter

* feat: map available MCP tools

* feat: load manifest tools

* feat: add MCP tools delimiter constant and update plugin key generation

* feat: call MCP tools

* feat: update librechat-data-provider version to 0.7.63 and enhance StdioOptionsSchema with additional properties

* refactor: simplify typing

* chore: update types/packages

* feat: MCP Tool Content parsing

* chore: update dependencies and improve package configurations

* feat: add 'mcp' directory to package and update configurations

* refactor: return CONTENT_AND_ARTIFACT format for MCP callTool

* chore: bump @librechat/agents

* WIP: MCP artifacts

* chore: bump @librechat/agents to v1.8.7

* fix: ensure filename has extension when saving base64 image

* fix: move base64 buffer conversion before filename extension check

* chore: update backend review workflow to install MCP package

* fix: use correct `mime` method

* fix: enhance file metadata with message and tool call IDs in image saving process

* fix: refactor ToolCall component to handle MCP tool calls and improve domain extraction

* fix: update ToolItem component for default isInstalled value and improve localization in ToolSelectDialog

* fix: update ToolItem component to use consistent text color for tool description

* style: add theming to ToolSelectDialog

* fix: improve domain extraction logic in ToolCall component

* refactor: conversation item theming, fix rename UI bug, optimize props, add missing types

* feat: enhance MCP options schema with base options (iconPath to start) and make transport type optional, infer based on other option fields

* fix: improve reconnection logic with parallel init and exponential backoff and enhance transport debug logging

* refactor: improve logging format

* refactor: improve logging of available tools by displaying tool names

* refactor: improve reconnection/connection logic

* feat: add MCP package build process to Dockerfile

* feat: add fallback icon for tools without an image in ToolItem component

* feat: Assistants Support for MCP Tools

* fix(build): configure rollup to use output.dir for dynamic imports

* chore: update @librechat/agents to version 1.8.8 and add @langchain/anthropic dependency

* fix: update CONFIG_VERSION to 1.2.0
  • Loading branch information
danny-avila authored Dec 17, 2024
1 parent 0a97ad3 commit e391347
Show file tree
Hide file tree
Showing 58 changed files with 4,322 additions and 234 deletions.
40 changes: 40 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*',
Expand Down Expand Up @@ -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',
Expand All @@ -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: {
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/backend-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
12 changes: 11 additions & 1 deletion Dockerfile.multi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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"]
34 changes: 31 additions & 3 deletions api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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.
Expand Down Expand Up @@ -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<string>} object.tools
* @param {boolean} [object.functions]
* @param {boolean} [object.returnMap]
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any> } | Record<string,Tool>>}
*/
const loadTools = async ({
user,
agent,
model,
isAgent,
endpoint,
useSpecs,
tools = [],
options = {},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]) {
Expand Down
17 changes: 17 additions & 0 deletions api/config/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
const { EventSource } = require('eventsource');
const logger = require('./winston');

global.EventSource = EventSource;

let mcpManager = null;

/**
* @returns {Promise<MCPManager>}
*/
async function getMCPManager() {
if (!mcpManager) {
const { MCPManager } = await import('librechat-mcp');
mcpManager = MCPManager.getInstance(logger);
}
return mcpManager;
}

module.exports = {
logger,
getMCPManager,
};
3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions api/server/controllers/PluginController.js
Original file line number Diff line number Diff line change
@@ -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');

/**
Expand Down Expand Up @@ -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);

Expand Down
54 changes: 51 additions & 3 deletions api/server/controllers/agents/callbacks.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const { Tools, StepTypes, imageGenTools } = require('librechat-data-provider');
const { Tools, StepTypes, imageGenTools, FileContext } = require('librechat-data-provider');
const {
EnvVar,
GraphEvents,
ToolEndHandler,
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');

Expand Down Expand Up @@ -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, {
Expand All @@ -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;
}
Expand Down
9 changes: 8 additions & 1 deletion api/server/services/AppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

/**
Expand Down Expand Up @@ -39,11 +40,17 @@ const AppService = async (app) => {

/** @type {Record<string, FunctionTool} */
const availableTools = loadAndFormatTools({
directory: paths.structuredTools,
adminFilter: filteredTools,
adminIncluded: includedTools,
directory: paths.structuredTools,
});

if (config.mcpServers != null) {
const mcpManager = await getMCPManager();
await mcpManager.initializeMCP(config.mcpServers);
await mcpManager.mapAvailableTools(availableTools);
}

const socialLogins =
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
const interfaceConfig = await loadDefaultInterface(config, configDefaults);
Expand Down
3 changes: 1 addition & 2 deletions api/server/services/Endpoints/agents/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ const initializeAgentOptions = async ({
}) => {
const { tools, toolContextMap } = await loadAgentTools({
req,
tools: agent.tools,
agent_id: agent.id,
agent,
tool_resources,
});

Expand Down
7 changes: 6 additions & 1 deletion api/server/services/Files/images/resize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

/**
Expand Down
Loading

0 comments on commit e391347

Please sign in to comment.