Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔧 feat: Initial MCP Support (Tools) #5015

Merged
merged 53 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
6218c52
📝 chore: Add comment to clarify purpose of check_updates.sh script
danny-avila Dec 10, 2024
5b1d734
feat: mcp package
danny-avila Dec 10, 2024
9d31b69
feat: add librechat-mcp package and update dependencies
danny-avila Dec 10, 2024
f93da8a
feat: refactor MCPConnectionSingleton to handle transport initializat…
danny-avila Dec 10, 2024
e1c4485
feat: change private methods to public in MCPConnectionSingleton for …
danny-avila Dec 10, 2024
f06ab3f
feat: filesystem demo
danny-avila Dec 10, 2024
a07ee76
chore: everything demo and move everything under mcp workspace
danny-avila Dec 10, 2024
8627f7c
chore: move ts-node to mcp workspace
danny-avila Dec 10, 2024
1910441
feat: mcp examples
danny-avila Dec 10, 2024
5b07ab9
feat: working sse MCP example
danny-avila Dec 11, 2024
1caea2b
refactor: rename MCPConnectionSingleton to MCPConnection for clarity
danny-avila Dec 11, 2024
3e422ab
refactor: replace MCPConnectionSingleton with MCPConnection for consi…
danny-avila Dec 11, 2024
4a17d08
refactor: manager/connections
danny-avila Dec 11, 2024
c7630cc
refactor: update MCPConnection to use type definitions from mcp types
danny-avila Dec 14, 2024
7d379e2
refactor: update MCPManager to use winston logger and enhance server …
danny-avila Dec 14, 2024
9cd1137
refactor: share logger between connections and manager
danny-avila Dec 14, 2024
f1fe61c
refactor: add schema definitions and update MCPManager to accept logg…
danny-avila Dec 14, 2024
8f33a38
feat: map available MCP tools
danny-avila Dec 15, 2024
5d237ff
feat: load manifest tools
danny-avila Dec 15, 2024
203d93e
feat: add MCP tools delimiter constant and update plugin key generation
danny-avila Dec 15, 2024
4b9f264
feat: call MCP tools
danny-avila Dec 15, 2024
f43812a
feat: update librechat-data-provider version to 0.7.63 and enhance St…
danny-avila Dec 15, 2024
c01111e
refactor: simplify typing
danny-avila Dec 15, 2024
b3425ac
chore: update types/packages
danny-avila Dec 15, 2024
786e96f
feat: MCP Tool Content parsing
danny-avila Dec 15, 2024
b0efbbd
chore: update dependencies and improve package configurations
danny-avila Dec 15, 2024
1741154
feat: add 'mcp' directory to package and update configurations
danny-avila Dec 15, 2024
5ea280a
refactor: return CONTENT_AND_ARTIFACT format for MCP callTool
danny-avila Dec 15, 2024
da15ac5
chore: bump @librechat/agents
danny-avila Dec 15, 2024
48158d0
WIP: MCP artifacts
danny-avila Dec 16, 2024
29a9d52
chore: bump @librechat/agents to v1.8.7
danny-avila Dec 16, 2024
0b62d7c
fix: ensure filename has extension when saving base64 image
danny-avila Dec 16, 2024
e5e3c0f
fix: move base64 buffer conversion before filename extension check
danny-avila Dec 16, 2024
dc77db9
chore: update backend review workflow to install MCP package
danny-avila Dec 16, 2024
08243de
fix: use correct `mime` method
danny-avila Dec 16, 2024
57c5604
fix: enhance file metadata with message and tool call IDs in image sa…
danny-avila Dec 16, 2024
3c74fbe
fix: refactor ToolCall component to handle MCP tool calls and improve…
danny-avila Dec 16, 2024
67d57f7
fix: update ToolItem component for default isInstalled value and impr…
danny-avila Dec 16, 2024
8b42396
fix: update ToolItem component to use consistent text color for tool …
danny-avila Dec 16, 2024
a36dd68
style: add theming to ToolSelectDialog
danny-avila Dec 16, 2024
1909658
fix: improve domain extraction logic in ToolCall component
danny-avila Dec 16, 2024
efad566
refactor: conversation item theming, fix rename UI bug, optimize prop…
danny-avila Dec 16, 2024
8c8412d
feat: enhance MCP options schema with base options (iconPath to start…
danny-avila Dec 16, 2024
fb8bd0c
fix: improve reconnection logic with parallel init and exponential ba…
danny-avila Dec 16, 2024
531f2fb
refactor: improve logging format
danny-avila Dec 16, 2024
af4b898
refactor: improve logging of available tools by displaying tool names
danny-avila Dec 16, 2024
6ee9d0b
refactor: improve reconnection/connection logic
danny-avila Dec 17, 2024
a1ca2a4
feat: add MCP package build process to Dockerfile
danny-avila Dec 17, 2024
05d7892
feat: add fallback icon for tools without an image in ToolItem component
danny-avila Dec 17, 2024
884c714
feat: Assistants Support for MCP Tools
danny-avila Dec 17, 2024
e0506eb
fix(build): configure rollup to use output.dir for dynamic imports
danny-avila Dec 17, 2024
6ad8da8
chore: update @librechat/agents to version 1.8.8 and add @langchain/a…
danny-avila Dec 17, 2024
ee3145f
fix: update CONFIG_VERSION to 1.2.0
danny-avila Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading