Skip to content

Commit ac17be5

Browse files
committed
Add httpBodyLimit config option for HTTP transport
Adds a new configuration option to control the maximum size of HTTP request bodies when using the HTTP transport. This addresses PayloadTooLargeError issues when clients send large JSON payloads. - Default value: 100kb (Express.js default) - Supports formats like '100kb', '1mb', '50mb' - Only applies when transport is 'http' Includes: - Unit tests for config parsing (CLI and env vars) - Integration tests for body limit enforcement - Updated configOverrides test for not-allowed fields
1 parent e9e5f0d commit ac17be5

File tree

19 files changed

+953
-80
lines changed

19 files changed

+953
-80
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
364364
| `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` / `--exportCleanupIntervalMs` | `120000` | Time in milliseconds between export cleanup cycles that remove expired export files. |
365365
| `MDB_MCP_EXPORT_TIMEOUT_MS` / `--exportTimeoutMs` | `300000` | Time in milliseconds after which an export is considered expired and eligible for cleanup. |
366366
| `MDB_MCP_EXPORTS_PATH` / `--exportsPath` | see below\* | Folder to store exported data files. |
367+
| `MDB_MCP_HTTP_BODY_LIMIT` / `--httpBodyLimit` | `"100kb"` | Maximum size of the HTTP request body (only used when transport is 'http'). Supports formats like '100kb', '1mb', '50mb'. |
367368
| `MDB_MCP_HTTP_HEADERS` / `--httpHeaders` | `"{}"` | Header that the HTTP server will validate when making requests (only used when transport is 'http'). |
368369
| `MDB_MCP_HTTP_HOST` / `--httpHost` | `"127.0.0.1"` | Host address to bind the HTTP server to (only used when transport is 'http'). |
369370
| `MDB_MCP_HTTP_PORT` / `--httpPort` | `3000` | Port number for the HTTP server (only used when transport is 'http'). Use 0 for a random port. |

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,24 @@
8787
"license": "Apache-2.0",
8888
"devDependencies": {
8989
"@ai-sdk/azure": "^2.0.53",
90-
"@emotion/css": "^11.13.5",
9190
"@ai-sdk/google": "^2.0.23",
9291
"@ai-sdk/mcp": "^0.0.8",
9392
"@ai-sdk/openai": "^2.0.52",
93+
"@emotion/css": "^11.13.5",
9494
"@eslint/js": "^9.34.0",
95+
"@leafygreen-ui/lib": "^15.7.0",
9596
"@leafygreen-ui/table": "^15.2.2",
97+
"@leafygreen-ui/tokens": "^4.2.0",
98+
"@leafygreen-ui/typography": "^22.2.3",
9699
"@modelcontextprotocol/inspector": "^0.17.1",
97100
"@mongodb-js/oidc-mock-provider": "^0.12.0",
98101
"@redocly/cli": "^2.0.8",
102+
"@testing-library/react": "^16.3.1",
99103
"@types/express": "^5.0.3",
100104
"@types/node": "^24.5.2",
101105
"@types/proper-lockfile": "^4.1.4",
102106
"@types/react": "^18.3.0",
103107
"@types/react-dom": "^19.2.3",
104-
"react": "^18.3.0",
105-
"react-dom": "^18.3.0",
106108
"@types/semver": "^7.7.0",
107109
"@types/yargs-parser": "^21.0.3",
108110
"@typescript-eslint/parser": "^8.44.0",
@@ -116,13 +118,16 @@
116118
"eslint-plugin-prettier": "^5.5.4",
117119
"globals": "^16.3.0",
118120
"husky": "^9.1.7",
121+
"jsdom": "^27.3.0",
119122
"knip": "^5.63.1",
120123
"mongodb": "^6.21.0",
121124
"mongodb-runner": "^6.2.0",
122125
"openapi-types": "^12.1.3",
123126
"openapi-typescript": "^7.9.1",
124127
"prettier": "^3.6.2",
125128
"proper-lockfile": "^4.1.2",
129+
"react": "^18.3.0",
130+
"react-dom": "^18.3.0",
126131
"semver": "^7.7.2",
127132
"simple-git": "^3.28.0",
128133
"testcontainers": "^11.7.1",

pnpm-lock.yaml

Lines changed: 356 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common/config/userConfig.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ const ServerConfigSchema = z4.object({
126126
"Header that the HTTP server will validate when making requests (only used when transport is 'http')."
127127
)
128128
.register(configRegistry, { overrideBehavior: "not-allowed" }),
129+
httpBodyLimit: z4
130+
.string()
131+
.regex(/^\d+(?:kb|mb|gb)$/i, "Invalid httpBodyLimit: must be a string like '100kb', '1mb', or '1gb'")
132+
.default("100kb")
133+
.describe(
134+
"Maximum size of the HTTP request body (only used when transport is 'http'). Supports formats like '100kb', '1mb', '50mb'. This is the Express.js json() middleware limit."
135+
)
136+
.register(configRegistry, { overrideBehavior: "not-allowed" }),
129137
idleTimeoutMs: z4.coerce
130138
.number()
131139
.default(600_000)

src/transports/streamableHttp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class StreamableHttpRunner extends TransportRunnerBase {
4242
);
4343

4444
app.enable("trust proxy"); // needed for reverse proxy support
45-
app.use(express.json());
45+
app.use(express.json({ limit: this.userConfig.httpBodyLimit }));
4646
app.use((req, res, next) => {
4747
for (const [key, value] of Object.entries(this.userConfig.httpHeaders)) {
4848
const header = req.headers[key.toLowerCase()];

src/ui/build/mount.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/// <reference types="vite/client" />
2+
import "../styles/fonts.css";
23
import React from "react";
34
import { createRoot } from "react-dom/client";
45

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { css } from "@emotion/css";
2+
import { color, InteractionState, Property, spacing, Variant } from "@leafygreen-ui/tokens";
3+
import { Theme } from "@leafygreen-ui/lib";
24

3-
export const tableStyles = css`
4-
background: white;
5+
export const getContainerStyles = (darkMode: boolean): string => css`
6+
background-color: ${color[darkMode ? Theme.Dark : Theme.Light][Property.Background][Variant.Primary][
7+
InteractionState.Default
8+
]};
9+
padding: ${spacing[200]}px;
10+
`;
11+
12+
export const AmountTextStyles = css`
13+
margin-bottom: ${spacing[400]}px;
514
`;
Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
import React from "react";
2-
import { useRenderData } from "../../hooks/index.js";
3-
import {
4-
Cell as LGCell,
5-
HeaderCell as LGHeaderCell,
6-
HeaderRow,
7-
Row as LGRow,
8-
Table,
9-
TableBody,
10-
TableHead,
11-
} from "@leafygreen-ui/table";
12-
import { tableStyles } from "./ListDatabases.styles.js";
1+
import { type ReactElement } from "react";
2+
import { useDarkMode, useRenderData } from "../../hooks/index.js";
3+
import { Cell, HeaderCell, HeaderRow, Row, Table, TableBody, TableHead } from "@leafygreen-ui/table";
4+
import { Body } from "@leafygreen-ui/typography";
135
import type { ListDatabasesOutput } from "../../../tools/mongodb/metadata/listDatabases.js";
6+
import { AmountTextStyles, getContainerStyles } from "./ListDatabases.styles.js";
147

15-
const HeaderCell = LGHeaderCell as React.FC<React.ComponentPropsWithoutRef<"th">>;
16-
const Cell = LGCell as React.FC<React.ComponentPropsWithoutRef<"td">>;
17-
const Row = LGRow as React.FC<React.ComponentPropsWithoutRef<"tr">>;
8+
export type Database = ListDatabasesOutput["databases"][number];
9+
10+
interface ListDatabasesProps {
11+
databases?: Database[];
12+
darkMode?: boolean;
13+
}
1814

1915
function formatBytes(bytes: number): string {
2016
if (bytes === 0) return "0 Bytes";
@@ -26,37 +22,49 @@ function formatBytes(bytes: number): string {
2622
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
2723
}
2824

29-
export const ListDatabases = (): React.ReactElement | null => {
30-
const { data, isLoading, error } = useRenderData<ListDatabasesOutput>();
25+
export const ListDatabases = ({
26+
databases: propDatabases,
27+
darkMode: darkModeProp,
28+
}: ListDatabasesProps): ReactElement | null => {
29+
const darkMode = useDarkMode(darkModeProp);
30+
const { data: hookData, isLoading, error } = useRenderData<ListDatabasesOutput>();
31+
const databases = propDatabases ?? hookData?.databases;
3132

32-
if (isLoading) {
33-
return <div>Loading...</div>;
34-
}
33+
if (!propDatabases) {
34+
if (isLoading) {
35+
return <div>Loading...</div>;
36+
}
3537

36-
if (error) {
37-
return <div>Error: {error}</div>;
38+
if (error) {
39+
return <div>Error: {error}</div>;
40+
}
3841
}
3942

40-
if (!data) {
43+
if (!databases) {
4144
return null;
4245
}
4346

4447
return (
45-
<Table className={tableStyles}>
46-
<TableHead>
47-
<HeaderRow>
48-
<HeaderCell>DB Name</HeaderCell>
49-
<HeaderCell>DB Size</HeaderCell>
50-
</HeaderRow>
51-
</TableHead>
52-
<TableBody>
53-
{data.databases.map((db) => (
54-
<Row key={db.name}>
55-
<Cell>{db.name}</Cell>
56-
<Cell>{formatBytes(db.size)}</Cell>
57-
</Row>
58-
))}
59-
</TableBody>
60-
</Table>
48+
<div className={getContainerStyles(darkMode)}>
49+
<Body className={AmountTextStyles} darkMode={darkMode}>
50+
Your cluster has <strong>{databases.length} databases</strong>:
51+
</Body>
52+
<Table darkMode={darkMode}>
53+
<TableHead>
54+
<HeaderRow>
55+
<HeaderCell>Database</HeaderCell>
56+
<HeaderCell>Size</HeaderCell>
57+
</HeaderRow>
58+
</TableHead>
59+
<TableBody>
60+
{databases.map((db) => (
61+
<Row key={db.name}>
62+
<Cell>{db.name}</Cell>
63+
<Cell>{formatBytes(db.size)}</Cell>
64+
</Row>
65+
))}
66+
</TableBody>
67+
</Table>
68+
</div>
6169
);
6270
};

src/ui/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export { useDarkMode } from "./useDarkMode.js";
12
export { useRenderData } from "./useRenderData.js";
3+
export { useHostCommunication } from "./useHostCommunication.js";

src/ui/hooks/useDarkMode.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useSyncExternalStore } from "react";
2+
3+
function subscribeToPrefersColorScheme(callback: () => void): () => void {
4+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
5+
mediaQuery.addEventListener("change", callback);
6+
return () => mediaQuery.removeEventListener("change", callback);
7+
}
8+
9+
function getPrefersDarkMode(): boolean {
10+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
11+
}
12+
13+
export function useDarkMode(override?: boolean): boolean {
14+
const prefersDarkMode = useSyncExternalStore(subscribeToPrefersColorScheme, getPrefersDarkMode);
15+
return override ?? prefersDarkMode;
16+
}

0 commit comments

Comments
 (0)