snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
147
apps/meteor/ee/server/api/mcp/dispatch.ts
Normal file
147
apps/meteor/ee/server/api/mcp/dispatch.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { McpTool } from './catalog';
|
||||
import type { McpAuth } from './server';
|
||||
|
||||
export type DispatchResult = {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
body: unknown;
|
||||
};
|
||||
|
||||
export type McpResponseBudget = {
|
||||
consume: (bytes: number) => void;
|
||||
};
|
||||
|
||||
const TOOL_CALL_TIMEOUT_MS = 20_000;
|
||||
const MAX_TOOL_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||
const BYTES_PER_MEBIBYTE = 1024 * 1024;
|
||||
|
||||
const formatByteLimit = (bytes: number): string =>
|
||||
bytes % BYTES_PER_MEBIBYTE === 0 ? `${bytes / BYTES_PER_MEBIBYTE} MiB` : `${bytes} bytes`;
|
||||
|
||||
export const createMcpResponseBudget = (maxBytes = MAX_TOOL_RESPONSE_BYTES): McpResponseBudget => {
|
||||
let remainingBytes = maxBytes;
|
||||
const formattedLimit = formatByteLimit(maxBytes);
|
||||
|
||||
return {
|
||||
consume(bytes) {
|
||||
if (bytes > remainingBytes) {
|
||||
throw new Error(`MCP batch response exceeds the ${formattedLimit} limit`);
|
||||
}
|
||||
|
||||
remainingBytes -= bytes;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const readResponseText = async (response: Response, responseBudget?: McpResponseBudget): Promise<string> => {
|
||||
const contentLength = Number(response.headers.get('content-length'));
|
||||
if (Number.isFinite(contentLength) && contentLength > MAX_TOOL_RESPONSE_BYTES) {
|
||||
throw new Error('MCP tool response exceeds the 5 MiB limit');
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const chunks: string[] = [];
|
||||
let receivedBytes = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
receivedBytes += value.byteLength;
|
||||
if (receivedBytes > MAX_TOOL_RESPONSE_BYTES) {
|
||||
await reader.cancel();
|
||||
throw new Error('MCP tool response exceeds the 5 MiB limit');
|
||||
}
|
||||
|
||||
try {
|
||||
responseBudget?.consume(value.byteLength);
|
||||
} catch (error) {
|
||||
await reader.cancel();
|
||||
throw error;
|
||||
}
|
||||
chunks.push(decoder.decode(value, { stream: true }));
|
||||
}
|
||||
|
||||
chunks.push(decoder.decode());
|
||||
return chunks.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute the REST endpoint a tool maps to, as the authenticated user.
|
||||
*
|
||||
* Dispatch is done via a loopback HTTP call to the local REST API, forwarding the
|
||||
* caller's PAT headers. This guarantees identical behaviour to a real REST client —
|
||||
* the same auth, permission checks, parameter validation and response shape — with
|
||||
* zero duplicated business logic. (An in-process Hono dispatch is a possible future
|
||||
* optimisation to avoid the loopback hop.)
|
||||
*
|
||||
* `clientIp` (resolved server-side from the MCP connection) is forwarded as `X-Real-IP`
|
||||
* so the REST per-route rate limiter keys on the real client rather than the loopback
|
||||
* address — otherwise every MCP caller would share a single `127.0.0.1` bucket.
|
||||
*/
|
||||
export const dispatchTool = async (
|
||||
tool: McpTool,
|
||||
args: Record<string, unknown>,
|
||||
auth: McpAuth,
|
||||
clientIp?: string,
|
||||
responseBudget?: McpResponseBudget,
|
||||
): Promise<DispatchResult> => {
|
||||
const port = process.env.PORT || '3000';
|
||||
const runtimeConfig = (
|
||||
globalThis as typeof globalThis & {
|
||||
__meteor_runtime_config__?: { ROOT_URL_PATH_PREFIX?: string };
|
||||
}
|
||||
).__meteor_runtime_config__;
|
||||
const base = `http://127.0.0.1:${port}${runtimeConfig?.ROOT_URL_PATH_PREFIX ?? ''}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-User-Id': auth.userId,
|
||||
'X-Auth-Token': auth.authToken,
|
||||
...(clientIp && { 'X-Real-IP': clientIp }),
|
||||
};
|
||||
|
||||
let url = base + tool.path;
|
||||
const init: RequestInit = {
|
||||
method: tool.method.toUpperCase(),
|
||||
headers,
|
||||
redirect: 'error',
|
||||
signal: AbortSignal.timeout(TOOL_CALL_TIMEOUT_MS),
|
||||
};
|
||||
|
||||
if (tool.method === 'get' || tool.method === 'delete') {
|
||||
const qs = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(args ?? {})) {
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
qs.append(key, typeof value === 'string' ? value : JSON.stringify(value));
|
||||
}
|
||||
const query = qs.toString();
|
||||
if (query) {
|
||||
url += `?${query}`;
|
||||
}
|
||||
} else {
|
||||
init.body = JSON.stringify(args ?? {});
|
||||
}
|
||||
|
||||
const res = await fetch(url, init);
|
||||
const responseText = await readResponseText(res, responseBudget);
|
||||
let body: unknown = responseText;
|
||||
if (responseText) {
|
||||
try {
|
||||
body = JSON.parse(responseText);
|
||||
} catch {
|
||||
// Keep non-JSON REST responses as text so callers receive the actual result.
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: res.ok, status: res.status, body };
|
||||
};
|
||||
Reference in New Issue
Block a user