Skip to content

Commit 92752a2

Browse files
Improve MCP functionality (#4709)
* Improve MCP functionality * apply ANSI strip path * attempt ARM build * improve dockerfile IO build time and migrate to ARM build * fix comment * add ability to disable MCP cooldown feature * update devbuild name * move chromium arm build patch to CDN
1 parent b54ac2d commit 92752a2

File tree

15 files changed

+370
-95
lines changed

15 files changed

+370
-95
lines changed

.github/workflows/dev-build.yaml

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
name: AnythingLLM Development Docker image (amd64)
1+
name: AnythingLLM Development Docker image (arm64)
22

33
concurrency:
44
group: build-${{ github.ref }}
55
cancel-in-progress: true
66

77
on:
88
push:
9-
branches: ['4686-feat-migrate-react-router-to-use-createbrowserrouter'] # put your current branch to create a build. Core team only.
9+
branches: ['mcp-improvements'] # put your current branch to create a build. Core team only.
1010
paths-ignore:
1111
- '**.md'
1212
- 'cloud-deployments/*'
@@ -21,9 +21,9 @@ on:
2121
- 'extras/**/*' # Extra is just for news and other local content.
2222

2323
jobs:
24-
push_multi_platform_to_registries:
25-
name: Push Docker multi-platform image to multiple registries
26-
runs-on: ubuntu-latest
24+
push_dev_build_to_dockerhub:
25+
name: Push development build image to Docker Hub
26+
runs-on: ubuntu-22.04-arm
2727
permissions:
2828
packages: write
2929
contents: read
@@ -78,8 +78,8 @@ jobs:
7878
push: true
7979
sbom: true
8080
provenance: mode=max
81-
platforms: linux/amd64
82-
# platforms: linux/amd64,linux/arm64
81+
# platforms: linux/amd64
82+
platforms: linux/arm64
8383
tags: ${{ steps.meta.outputs.tags }}
8484
labels: ${{ steps.meta.outputs.labels }}
8585
cache-from: type=gha
@@ -88,37 +88,37 @@ jobs:
8888
# For Docker scout there are some intermediary reported CVEs which exists outside
8989
# of execution content or are unreachable by an attacker but exist in image.
9090
# We create VEX files for these so they don't show in scout summary.
91-
- name: Collect known and verified CVE exceptions
92-
id: cve-list
93-
run: |
94-
# Collect CVEs from filenames in vex folder
95-
CVE_NAMES=""
96-
for file in ./docker/vex/*.vex.json; do
97-
[ -e "$file" ] || continue
98-
filename=$(basename "$file")
99-
stripped_filename=${filename%.vex.json}
100-
CVE_NAMES+=" $stripped_filename"
101-
done
102-
echo "CVE_EXCEPTIONS=$CVE_NAMES" >> $GITHUB_OUTPUT
103-
shell: bash
91+
# - name: Collect known and verified CVE exceptions
92+
# id: cve-list
93+
# run: |
94+
# # Collect CVEs from filenames in vex folder
95+
# CVE_NAMES=""
96+
# for file in ./docker/vex/*.vex.json; do
97+
# [ -e "$file" ] || continue
98+
# filename=$(basename "$file")
99+
# stripped_filename=${filename%.vex.json}
100+
# CVE_NAMES+=" $stripped_filename"
101+
# done
102+
# echo "CVE_EXCEPTIONS=$CVE_NAMES" >> $GITHUB_OUTPUT
103+
# shell: bash
104104

105105
# About VEX attestations https://docs.docker.com/scout/explore/exceptions/
106106
# Justifications https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications
107107
# Fixed to use v1.15.1 of scout-cli as v1.16.0 install script is broken
108108
# https://github.com/docker/scout-cli
109-
- name: Add VEX attestations
110-
env:
111-
CVE_EXCEPTIONS: ${{ steps.cve-list.outputs.CVE_EXCEPTIONS }}
112-
run: |
113-
echo $CVE_EXCEPTIONS
114-
curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --
115-
for cve in $CVE_EXCEPTIONS; do
116-
for tag in "${{ join(fromJSON(steps.meta.outputs.json).tags, ' ') }}"; do
117-
echo "Attaching VEX exception $cve to $tag"
118-
docker scout attestation add \
119-
--file "./docker/vex/$cve.vex.json" \
120-
--predicate-type https://openvex.dev/ns/v0.2.0 \
121-
$tag
122-
done
123-
done
124-
shell: bash
109+
# - name: Add VEX attestations
110+
# env:
111+
# CVE_EXCEPTIONS: ${{ steps.cve-list.outputs.CVE_EXCEPTIONS }}
112+
# run: |
113+
# echo $CVE_EXCEPTIONS
114+
# curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --
115+
# for cve in $CVE_EXCEPTIONS; do
116+
# for tag in "${{ join(fromJSON(steps.meta.outputs.json).tags, ' ') }}"; do
117+
# echo "Attaching VEX exception $cve to $tag"
118+
# docker scout attestation add \
119+
# --file "./docker/vex/$cve.vex.json" \
120+
# --predicate-type https://openvex.dev/ns/v0.2.0 \
121+
# $tag
122+
# done
123+
# done
124+
# shell: bash

docker/.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,9 @@ GID='1000'
400400

401401
# Disable Swagger API documentation endpoint.
402402
# Set to "true" to disable the /api/docs endpoint (recommended for production deployments).
403-
# DISABLE_SWAGGER_DOCS="true"
403+
# DISABLE_SWAGGER_DOCS="true"
404+
405+
# Disable MCP cooldown timer for agent calls
406+
# this can lead to infinite recursive calls of the same function
407+
# for some model/provider combinations
408+
# MCP_NO_COOLDOWN="true

docker/Dockerfile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ WORKDIR /app
5858
# so web-scraping would be broken in arm docker containers unless we patch it
5959
# by manually installing a compatible chromedriver.
6060
RUN echo "Need to patch Puppeteer x Chromium support for ARM86 - installing dep!" && \
61-
curl https://playwright.azureedge.net/builds/chromium/1088/chromium-linux-arm64.zip -o chrome-linux.zip && \
61+
curl -fSL https://webassets.anythingllm.com/chromium-1088-linux-arm64.zip -o chrome-linux.zip && \
6262
unzip chrome-linux.zip && \
6363
rm -rf chrome-linux.zip
6464

@@ -161,10 +161,6 @@ USER anythingllm
161161
FROM backend-build AS production-build
162162
WORKDIR /app
163163
COPY --chown=anythingllm:anythingllm --from=frontend-build /app/frontend/dist /app/server/public
164-
USER root
165-
RUN chown -R anythingllm:anythingllm /app/server && \
166-
chown -R anythingllm:anythingllm /app/collector
167-
USER anythingllm
168164

169165
# Setup the environment
170166
ENV NODE_ENV=production

server/.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,4 +403,9 @@ TTS_PROVIDER="native"
403403

404404
# Disable Swagger API documentation endpoint.
405405
# Set to "true" to disable the /api/docs endpoint (recommended for production deployments).
406-
# DISABLE_SWAGGER_DOCS="true"
406+
# DISABLE_SWAGGER_DOCS="true"
407+
408+
# Disable MCP cooldown timer for agent calls
409+
# this can lead to infinite recursive calls of the same function
410+
# for some model/provider combinations
411+
# MCP_NO_COOLDOWN="true

server/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"express": "^4.18.2",
5454
"extract-json-from-string": "^1.0.1",
5555
"fast-levenshtein": "^3.0.0",
56+
"fix-path": "^4.0.0",
5657
"graphql": "^16.7.1",
5758
"ip": "^2.0.1",
5859
"joi": "^17.11.0",
@@ -74,6 +75,7 @@
7475
"posthog-node": "^3.1.1",
7576
"prisma": "5.3.1",
7677
"slugify": "^1.6.6",
78+
"strip-ansi": "^7.1.2",
7779
"swagger-autogen": "^2.23.5",
7880
"swagger-ui-express": "^5.0.0",
7981
"truncate": "^3.0.0",
@@ -101,4 +103,4 @@
101103
"nodemon": "^2.0.22",
102104
"prettier": "^3.0.3"
103105
}
104-
}
106+
}

server/utils/MCP/hypervisor/index.js

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ class MCPHypervisor {
193193

194194
this.log(`Pruning MCP server: ${name}`);
195195
const mcp = this.mcps[name];
196+
if (!mcp.transport) return true;
196197
const childProcess = mcp.transport._process;
197-
if (childProcess) childProcess.kill(1);
198+
if (childProcess) childProcess.kill("SIGTERM");
198199
mcp.transport.close();
199200

200201
delete this.mcps[name];
@@ -215,10 +216,11 @@ class MCPHypervisor {
215216
for (const name of Object.keys(this.mcps)) {
216217
if (!this.mcps[name]) continue;
217218
const mcp = this.mcps[name];
219+
if (!mcp.transport) continue;
218220
const childProcess = mcp.transport._process;
219221
if (childProcess)
220222
this.log(`Killing MCP ${name} (PID: ${childProcess.pid})`, {
221-
killed: childProcess.kill(1),
223+
killed: childProcess.kill("SIGTERM"),
222224
});
223225

224226
mcp.transport.close();
@@ -228,18 +230,51 @@ class MCPHypervisor {
228230
this.mcpLoadingResults = {};
229231
}
230232

233+
/**
234+
* Load shell environment for desktop applications.
235+
* MacOS and Linux don't inherit login shell environment. So this function
236+
* fixes the PATH and accessible commands when running AnythingLLM outside of Docker during development on Mac/Linux and in-container (Linux).
237+
* @returns {Promise<{[key: string]: string}>} - Environment variables from shell
238+
*/
239+
async #loadShellEnvironment() {
240+
try {
241+
if (process.platform === "win32") return process.env;
242+
const { default: fixPath } = await import("fix-path");
243+
const { default: stripAnsi } = await import("strip-ansi");
244+
fixPath();
245+
246+
// Due to node v20 requirement to have a minimum version of fix-path v5, we need to strip ANSI codes manually
247+
// which was the only patch between v4 and v5. Here we just apply manually.
248+
// https://github.com/sindresorhus/fix-path/issues/6
249+
if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);
250+
return process.env;
251+
} catch (error) {
252+
console.warn(
253+
"Failed to load shell environment, using process.env:",
254+
error.message
255+
);
256+
return process.env;
257+
}
258+
}
259+
231260
/**
232261
* Build the MCP server environment variables - ensures proper PATH and NODE_PATH
233262
* inheritance across all platforms and deployment scenarios.
234263
* @param {Object} server - The server definition
235-
* @returns {{env: { [key: string]: string } | {}}} - The environment variables
264+
* @returns {Promise<{env: { [key: string]: string } | {}}}> - The environment variables
236265
*/
237-
#buildMCPServerENV(server) {
238-
// Start with essential environment variables, inheriting from current process
239-
// This ensures GUI applications on macOS/Linux get proper PATH inheritance
266+
async #buildMCPServerENV(server) {
267+
const shellEnv = await this.#loadShellEnvironment();
240268
let baseEnv = {
241-
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
242-
NODE_PATH: process.env.NODE_PATH || "/usr/local/lib/node_modules",
269+
PATH:
270+
shellEnv.PATH ||
271+
process.env.PATH ||
272+
"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
273+
NODE_PATH:
274+
shellEnv.NODE_PATH ||
275+
process.env.NODE_PATH ||
276+
"/usr/local/lib/node_modules",
277+
...shellEnv, // Include all shell environment variables
243278
};
244279

245280
// Docker-specific environment setup
@@ -273,29 +308,57 @@ class MCPHypervisor {
273308
* @returns {MCPServerTypes | null} - The server type
274309
*/
275310
#parseServerType(server) {
276-
if (server.hasOwnProperty("command")) return "stdio";
277-
if (server.hasOwnProperty("url")) return "http";
311+
if (
312+
server.type === "sse" ||
313+
server.type === "streamable" ||
314+
server.type === "http"
315+
)
316+
return "http";
317+
if (Object.prototype.hasOwnProperty.call(server, "command")) return "stdio";
318+
if (Object.prototype.hasOwnProperty.call(server, "url")) return "http";
278319
return "sse";
279320
}
280321

281322
/**
282323
* Validate the server definition by type
283324
* - Will throw an error if the server definition is invalid
325+
* @param {string} name - The name of the MCP server
284326
* @param {Object} server - The server definition
285327
* @param {MCPServerTypes} type - The server type
286328
* @returns {void}
287329
*/
288-
#validateServerDefinitionByType(server, type) {
330+
#validateServerDefinitionByType(name, server, type) {
331+
if (
332+
server.type === "sse" ||
333+
server.type === "streamable" ||
334+
server.type === "http"
335+
) {
336+
if (!server.url) {
337+
throw new Error(
338+
`MCP server "${name}": missing required "url" for ${server.type} transport`
339+
);
340+
}
341+
342+
try {
343+
new URL(server.url);
344+
} catch (error) {
345+
throw new Error(`MCP server "${name}": invalid URL "${server.url}"`);
346+
}
347+
return;
348+
}
349+
289350
if (type === "stdio") {
290-
if (server.hasOwnProperty("args") && !Array.isArray(server.args))
351+
if (
352+
Object.prototype.hasOwnProperty.call(server, "args") &&
353+
!Array.isArray(server.args)
354+
)
291355
throw new Error("MCP server args must be an array");
292356
}
293357

294358
if (type === "http") {
295359
if (!["sse", "streamable"].includes(server?.type))
296360
throw new Error("MCP server type must have sse or streamable value.");
297361
}
298-
299362
if (type === "sse") return;
300363
return;
301364
}
@@ -304,16 +367,16 @@ class MCPHypervisor {
304367
* Setup the server transport by type and server definition
305368
* @param {Object} server - The server definition
306369
* @param {MCPServerTypes} type - The server type
307-
* @returns {StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport} - The server transport
370+
* @returns {Promise<StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport>} - The server transport
308371
*/
309-
#setupServerTransport(server, type) {
372+
async #setupServerTransport(server, type) {
310373
// if not stdio then it is http or sse
311374
if (type !== "stdio") return this.createHttpTransport(server);
312375

313376
return new StdioClientTransport({
314377
command: server.command,
315378
args: server?.args ?? [],
316-
...this.#buildMCPServerENV(server),
379+
...(await this.#buildMCPServerENV(server)),
317380
});
318381
}
319382

@@ -328,6 +391,7 @@ class MCPHypervisor {
328391
// If the server block has a type property then use that to determine the transport type
329392
switch (server.type) {
330393
case "streamable":
394+
case "http":
331395
return new StreamableHTTPClientTransport(url, {
332396
requestInit: {
333397
headers: server.headers,
@@ -354,10 +418,10 @@ class MCPHypervisor {
354418
const serverType = this.#parseServerType(server);
355419
if (!serverType) throw new Error("MCP server command or url is required");
356420

357-
this.#validateServerDefinitionByType(server, serverType);
421+
this.#validateServerDefinitionByType(name, server, serverType);
358422
this.log(`Attempting to start MCP server: ${name}`);
359423
const mcp = new Client({ name: name, version: "1.0.0" });
360-
const transport = this.#setupServerTransport(server, serverType);
424+
const transport = await this.#setupServerTransport(server, serverType);
361425

362426
// Add connection event listeners
363427
transport.onclose = () => this.log(`${name} - Transport closed`);
@@ -369,10 +433,22 @@ class MCPHypervisor {
369433
// Connect and await the connection with a timeout
370434
this.mcps[name] = mcp;
371435
const connectionPromise = mcp.connect(transport);
436+
437+
let timeoutId;
372438
const timeoutPromise = new Promise((_, reject) => {
373-
setTimeout(() => reject(new Error("Connection timeout")), 30_000); // 30 second timeout
439+
timeoutId = setTimeout(
440+
() => reject(new Error("Connection timeout")),
441+
30_000
442+
); // 30 second timeout
374443
});
375-
await Promise.race([connectionPromise, timeoutPromise]);
444+
445+
try {
446+
await Promise.race([connectionPromise, timeoutPromise]);
447+
if (timeoutId) clearTimeout(timeoutId);
448+
} catch (error) {
449+
if (timeoutId) clearTimeout(timeoutId);
450+
throw error;
451+
}
376452
return true;
377453
}
378454

0 commit comments

Comments
 (0)