Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 23 additions & 12 deletions DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,32 @@ The MCP server requires Cloudinary credentials to run. You can provide these cre
This is the recommended method.

```sh
docker run -d -p 2718:2718 \
docker run -d -p 127.0.0.1:2718:2718 \
-e CLOUDINARY_CLOUD_NAME="<your_cloud_name>" \
-e CLOUDINARY_API_KEY="<your_api_key>" \
-e CLOUDINARY_API_SECRET="<your_api_secret>" \
cloudinary-analysis-mcp start --transport sse
cloudinary-analysis-mcp start --transport sse --host 0.0.0.0
```

**Note:** If you have these variables already set in your shell environment, you can pass them directly to the container without specifying the values:
**Note:** The `--host 0.0.0.0` flag is required inside Docker containers for port mapping to work. The `-p 127.0.0.1:2718:2718` ensures the port is only exposed on your machine's localhost, not to the network.

If you have these variables already set in your shell environment, you can pass them directly to the container without specifying the values:

```sh
docker run -d -p 2718:2718 \
docker run -d -p 127.0.0.1:2718:2718 \
-e CLOUDINARY_CLOUD_NAME \
-e CLOUDINARY_API_KEY \
-e CLOUDINARY_API_SECRET \
cloudinary-analysis-mcp start --transport sse
cloudinary-analysis-mcp start --transport sse --host 0.0.0.0
```

### Option 2: Using Command-Line Arguments

You can also provide the credentials as arguments to the `start` command.

```sh
docker run -d -p 2718:2718 \
cloudinary-analysis-mcp start --transport sse \
docker run -d -p 127.0.0.1:2718:2718 \
cloudinary-analysis-mcp start --transport sse --host 0.0.0.0 \
--cloud-name "<your_cloud_name>" \
--api-key "<your_api_key>" \
--api-secret "<your_api_secret>"
Expand All @@ -67,15 +69,15 @@ docker run -d -p 2718:2718 \
This method combines all credentials into a single URL.

```sh
docker run -d -p 2718:2718 \
docker run -d -p 127.0.0.1:2718:2718 \
-e CLOUDINARY_URL="cloudinary://<your_api_key>:<your_api_secret>@<your_cloud_name>" \
cloudinary-analysis-mcp start --transport sse
cloudinary-analysis-mcp start --transport sse --host 0.0.0.0
```

**Note:** If you have the `CLOUDINARY_URL` variable already set in your shell environment, you can pass it directly:

```sh
docker run -d -p 2718:2718 -e CLOUDINARY_URL cloudinary-analysis-mcp start --transport sse
docker run -d -p 127.0.0.1:2718:2718 -e CLOUDINARY_URL cloudinary-analysis-mcp start --transport sse --host 0.0.0.0
```

## Connecting to the Server
Expand Down Expand Up @@ -134,7 +136,7 @@ Set the `--log-level` flag to `debug` when starting the container.
```sh
docker run -d -p 2718:2718 \
-e CLOUDINARY_URL \
cloudinary-analysis-mcp start --transport sse --log-level debug
cloudinary-analysis-mcp start --transport sse --host 0.0.0.0 --log-level debug
```

### Using the `CLOUDINARY_DEBUG` Environment Variable
Expand All @@ -145,6 +147,15 @@ You can also enable a debug logger by setting the `CLOUDINARY_DEBUG` environment
docker run -d -p 2718:2718 \
-e CLOUDINARY_URL \
-e CLOUDINARY_DEBUG=true \
cloudinary-analysis-mcp start --transport sse
cloudinary-analysis-mcp start --transport sse --host 0.0.0.0
```

## Security Considerations

The MCP server handles your Cloudinary API credentials. To prevent unauthorized access:

- **Localhost binding (default):** When running outside Docker, the server binds to `127.0.0.1` by default, making it accessible only from your local machine.
- **Docker port mapping:** Use `-p 127.0.0.1:2718:2718` (not `-p 2718:2718`) to restrict Docker port exposure to localhost only. The broader form exposes the port to your entire network.
- **`--host 0.0.0.0`:** Only use this inside Docker containers (where it's required for port mapping) or when you explicitly need network access. Never use this on a host machine without additional access controls.
- **`--allowed-origins`:** When using the `serve` command with cross-origin clients, specify allowed origins explicitly (e.g., `--allowed-origins http://localhost:3000`) instead of allowing all origins.

15 changes: 15 additions & 0 deletions src/mcp-server/cli/serve/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ export const serveCommand = buildCommand({
parse: (val: string) =>
z.coerce.number().int().gte(0).lt(65536).parse(val),
},
host: {
kind: "parsed",
brief:
"The host address to bind to (default: 127.0.0.1). Use 0.0.0.0 to listen on all interfaces (SECURITY: exposes server to the network)",
default: "127.0.0.1",
parse: (value) => z.string().parse(value),
},
"allowed-origins": {
kind: "parsed",
brief:
"Allowed CORS origins (comma-separated). Only requests from these origins will receive CORS headers. Required when accessed cross-origin.",
optional: true,
variadic: true,
parse: (value) => z.string().url("Must be a valid origin URL").parse(value),
},
"disable-static-auth": {
kind: "boolean",
brief:
Expand Down
36 changes: 31 additions & 5 deletions src/mcp-server/cli/serve/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import { landingPageExpress } from "../../../landing-page.js";

interface ServeCommandFlags extends MCPServerFlags {
readonly port: number;
readonly host: string;
readonly "disable-static-auth": boolean;
readonly "allowed-origins"?: string[];
readonly "log-level": ConsoleLoggerLevel;
readonly env?: [string, string][];
}
Expand All @@ -37,11 +39,29 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) {
const logger = createConsoleLogger(cliFlags["log-level"]);
const app = express();

// Enable CORS for cross-origin requests
const allowedOrigins = cliFlags["allowed-origins"];

// Origin validation middleware (replaces wildcard CORS)
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.header("Access-Control-Allow-Headers", "*");
const origin = req.headers.origin;

if (allowedOrigins && allowedOrigins.length > 0 && origin) {
if (allowedOrigins.includes(origin)) {
res.header("Access-Control-Allow-Origin", origin);
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id, Last-Event-Id, api-key, api-secret, cloud-name, o-auth2");
res.header("Vary", "Origin");
} else {
// Origin not allowed — reject preflight, omit CORS headers on actual requests
if (req.method === "OPTIONS") {
res.sendStatus(403);
return;
}
}
}
// When no allowed-origins configured and binding to localhost, no CORS headers
// are needed (same-origin by default). This is the secure default.

if (req.method === "OPTIONS") {
res.sendStatus(204);
return;
Expand Down Expand Up @@ -88,10 +108,16 @@ async function startStreamableHTTP(cliFlags: ServeCommandFlags) {

app.get("/", landingPageExpress);

const httpServer = app.listen(cliFlags.port, "0.0.0.0", () => {
const httpServer = app.listen(cliFlags.port, cliFlags.host, () => {
const ha = httpServer.address();
const host = typeof ha === "string" ? ha : `${ha?.address}:${ha?.port}`;
logger.info("MCP Streamable HTTP server started", { host });
if (cliFlags.host === "0.0.0.0") {
logger.warning(
"Server is listening on all interfaces (0.0.0.0). " +
"This exposes the server to the network. Use --host 127.0.0.1 for localhost-only access."
);
}
});

const shutdown = () => {
Expand Down
7 changes: 7 additions & 0 deletions src/mcp-server/cli/start/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export const startCommand = buildCommand({
parse: (val: string) =>
z.coerce.number().int().gte(0).lt(65536).parse(val),
},
host: {
kind: "parsed",
brief:
"The host address to bind to (default: 127.0.0.1). Use 0.0.0.0 to listen on all interfaces (SECURITY: exposes server to the network)",
default: "127.0.0.1",
parse: (value) => z.string().parse(value),
},
tool: {
kind: "parsed",
brief: "Specify tools to mount on the server",
Expand Down
9 changes: 8 additions & 1 deletion src/mcp-server/cli/start/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { landingPageExpress } from "../../../landing-page.js";
interface StartCommandFlags extends MCPServerFlags {
readonly transport: "stdio" | "sse";
readonly port: number;
readonly host: string;
readonly "log-level": ConsoleLoggerLevel;
readonly env?: [string, string][];
}
Expand Down Expand Up @@ -191,10 +192,16 @@ async function startSSE(cliFlags: StartCommandFlags) {

app.get("/", landingPageExpress);

const httpServer = app.listen(cliFlags.port, "0.0.0.0", () => {
const httpServer = app.listen(cliFlags.port, cliFlags.host, () => {
const ha = httpServer.address();
const host = typeof ha === "string" ? ha : `${ha?.address}:${ha?.port}`;
logger.info("MCP HTTP server started", { host });
if (cliFlags.host === "0.0.0.0") {
logger.warning(
"Server is listening on all interfaces (0.0.0.0). " +
"This exposes the server to the network. Use --host 127.0.0.1 for localhost-only access."
);
}
});

let closing = false;
Expand Down