Chronoloq
    Blog & Threat Research
    Security ResearchMoaz Ehab June 8, 2026 8 min read

    Bad MCP Config vs Good MCP Config: The Three Files That Decide Whether Your Agent Is Safe

    Most MCP security incidents trace back to three configuration files: the server manifest, the tool-call handler, and the deployment spec. Here's the bad version and the good version at each layer, with the reasoning for every change.

    Most Model Context Protocol (MCP) security incidents don't start with a sophisticated model failure. They start with a configuration choice that seemed reasonable during a demo: no authentication because it was internal-only at the time, a tool granted broad access because scoping it felt like premature optimization, a container running with default permissions because nobody circled back before it shipped. OWASP's dedicated MCP Top 10 lists token mismanagement and secret exposure as its first category for exactly this reason (OWASP Foundation, 2025), and independent research into MCP RCE vulnerabilities and tool-poisoning attacks has repeatedly traced real exploitation paths back to these same, ordinary config gaps rather than to novel model jailbreaks.

    This post walks through three specific files that determine whether an MCP deployment is defensible: the server manifest and auth config, the tool-call handler, and the deployment/container spec. Bad and good versions of each, with the reasoning for what changed.

    File 1: the server manifest and authorization config

    The manifest is where scope, trust, and auth get decided, or don't. The most common failure is treating it as internal documentation instead of an enforced security boundary.

    yamlcode
    # BAD: bad-mcp-manifest.yaml# No auth, unscoped tool access, plaintext secret, no approval gatingname: finance-ops-servertransport: httpauth: none  # "it's just for internal use"tools:  - name: query_database    description: "Run a query against the finance database"    scopes: ["*"]              # unscoped: full read/write access  - name: send_wire_transfer    description: "Initiate a wire transfer"    requires_approval: false   # destructive action, no human gatesecrets:  db_password: "p@ssw0rd123"   # hardcoded, in the config file
    yamlcode
    # GOOD: good-mcp-manifest.yaml# OAuth 2.1 resource server, scoped tools, human approval on destructive ops,# secrets resolved from a vault reference, not stored in configname: finance-ops-servertransport: httpauth:  type: oauth2.1  mode: resource_server         # MCP server validates tokens; does not issue them  audience: "finance-ops-server" # RFC 8707 resource indicator, binds tokens to this server  authorization_server: "https://auth.internal.example.com"  token_validation:    require_audience_match: true    reject_unbound_tokens: truetools:  - name: query_database    description: "Run a read-only query against the finance database"    scopes: ["finance:read"]     # least privilege, not "*"    description_hash: "sha256:4f2c...a91"  # pinned; a silent edit breaks the hash  - name: send_wire_transfer    description: "Initiate a wire transfer"    scopes: ["finance:write", "finance:transfer"]    risk_tier: destructive    requires_approval: true      # explicit human gate, enforced by the clientsecrets:  db_password:    from: vault    path: "secret/finance-ops/db_password"  # resolved at runtime, never written to disk

    Four changes carry the actual risk reduction. The auth block makes the server an OAuth 2.1 resource server that validates a token's audience before accepting it, exactly what the MCP specification's 2025-06-18 authorization revision requires via RFC 8707 resource indicators, so a token minted for a different MCP server can't be replayed against this one. Tool scopes go from ["*"] to named, least-privilege scopes. The wire transfer tool, a genuinely destructive action, is explicitly tagged and gated behind human approval rather than left to whatever the client happens to do by default, matching the MCP specification's own expectation that a human stay in the loop for actions that write, delete, or move money. And the description hash addresses a documented attack class: MCP tool descriptions are read by the model as trusted context every time it decides which tool to call, which makes an unpinned description an editable attack surface, the mechanism behind real "rug pull" and tool-poisoning exploits researchers have already demonstrated against production MCP servers (Invariant Labs, 2026).

    File 2: the tool-call handler

    The manifest declares intent. The handler is where that intent is actually enforced, or silently isn't.

    pythoncode
    # BAD: handler executes whatever arguments the model produced,# no validation, no allow-listing, direct shell executiondef handle_tool_call(tool_name, arguments):    if tool_name == "run_report":        # arguments["query"] comes straight from the model's tool call        result = subprocess.run(            arguments["query"], shell=True, capture_output=True        )        return result.stdout
    pythoncode
    # GOOD: validates and allow-lists before executing anything,# no shell interpolation, scoped to a fixed set of safe operationsALLOWED_REPORTS = {"monthly_revenue", "quarterly_headcount", "open_invoices"}def handle_tool_call(tool_name, arguments, caller_scopes):    if tool_name != "run_report":        raise ToolNotFoundError(tool_name)    if "finance:read" not in caller_scopes:        raise PermissionDeniedError(tool_name)    report_id = arguments.get("report_id")    if report_id not in ALLOWED_REPORTS:        raise InvalidArgumentError(f"Unknown report_id: {report_id}")    # Fixed, parameterized lookup. No user- or model-controlled string    # ever reaches a shell or a raw SQL statement.    return REPORT_REGISTRY[report_id].run()

    The bad version has two independent failures stacked on top of each other: it trusts model-generated input as if it were developer-written code, and it passes that input to shell=True, which turns any successful prompt injection or tool-hijack into arbitrary command execution. The fix isn't "sanitize the string harder." It's removing the class of input entirely, the model selects from a fixed, pre-approved set of operations rather than supplying a string that gets executed, and the scope check happens against the caller's actual granted permissions rather than being assumed from the fact that the call reached the handler at all.

    File 3: the deployment spec

    Even a correctly scoped, correctly validated MCP server should assume it will eventually be compromised, through a dependency vulnerability, a supply-chain issue in an SDK, or a flaw in the protocol implementation itself, and be deployed so that compromise doesn't spread.

    yamlcode
    # BAD: docker-compose.yml# Root user, host network, unrestricted egress, no resource limitsservices:  mcp-server:    image: finance-mcp:latest    network_mode: host        # server can reach anything the host can    privileged: true          # runs as root, effectively    environment:      - ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxx  # secret baked into the compose file
    yamlcode
    # GOOD: docker-compose.yml# Non-root, isolated network, explicit egress allow-list, resource capsservices:  mcp-server:    image: finance-mcp:1.4.2   # pinned version, not :latest    user: "10001:10001"        # non-root    read_only: true    security_opt:      - no-new-privileges:true    networks:      - mcp-isolated            # dedicated network, not host    deploy:      resources:        limits:          cpus: "1.0"          memory: 512M    environment:      - ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key  # mounted secret, not inline    secrets:      - anthropic_api_keynetworks:  mcp-isolated:    driver: bridge    internal: true             # no direct route to the public internetsecrets:  anthropic_api_key:    external: true

    Container isolation is the layer that limits blast radius when the first two layers fail anyway, and it's the layer 2026's MCP RCE research made most relevant: an architectural vulnerability in MCP's official SDKs, disclosed by researchers earlier this year, enabled arbitrary remote code execution across a supply chain touching more than 150 million downloads and up to 200,000 server instances (Tom's Hardware, 2026). A pinned, non-root, network-isolated container doesn't prevent that class of SDK vulnerability. It determines whether exploiting it gets an attacker a foothold in one disposable container or a path to the host and everything else reachable from it.

    A short checklist

    1. 1Authenticate every MCP server as an OAuth 2.1 resource server, and validate token audience so a credential minted for one server can't be replayed against another.
    2. 2Scope tools to named permissions, never ["*"], and tag destructive operations for mandatory human approval rather than relying on client-side defaults.
    3. 3Pin tool descriptions. A tool description is trusted input to the model; treat an unreviewed change to it the same way you'd treat an unreviewed change to a permissions file.
    4. 4Never let model-generated arguments reach a shell, raw SQL, or an unbounded file path. Validate against a fixed set of allowed operations before executing anything.
    5. 5Deploy MCP servers like any other high-privilege service: pinned images, non-root, isolated networks, resource limits, and secrets from a mount or vault, never inline in a compose file or manifest.

    None of these fixes require exotic tooling. They require treating an MCP server's configuration with the same scrutiny a team would already apply to a service holding production database credentials, because functionally, that's what it is. Chronoloq's LLM Shield is built to catch the gap when that scrutiny doesn't happen: it covers RAG, A2A, and MCP-based architectures specifically, flagging unscoped tool permissions, missing approval gates, and exposed secrets before they ship rather than after a research team finds them.


    Chronoloq

    See the vulnerabilities covered in this research, in your own APIs

    Chronoloq scans for OWASP API Security Top 10, OWASP LLM Top 10, and agentic vulnerabilities including MCP, A2A, and RAG attack surfaces.

    Book a security review

    This website uses cookies to enhance user experience and to analyze performance and traffic on our website. We also share information about your use of our site with our social media, advertising and analytics partners. View Cookie Policy