Module 7: Connecting to Claude Code and Third-Party Servers

A server's trust boundary

Description

Lesson 05 showed .mcp.json has no field at all to distinguish a trusted server from an untrusted one — the registration mechanism is identical for both. This lesson answers the question left hanging: if the file doesn't protect you, what exactly are you trusting when you add a new entry to mcpServers? The answer depends on the transport, and it's more concrete than it first appears: a stdio server receives code execution on your own machine, with your own operating-system permissions; an http server receives your data, traveling to a process someone else operates. Naming this precisely — without yet building any defense — is this lesson's whole goal.

Connection to the module

This lesson sets the stage for lesson 07, which focuses on a specific, sharper case of this boundary: the text a server returns (a tool's description, a resource's content) as data crossing that same boundary, with the protocol not marking it as special in any way.


What you exactly give a stdio server: local code execution

When you register a type: "stdio" entry, the host — Claude Code, or the subprocess.Popen you wrote yourself in Modules 2 through 6 — launches a new operating-system process, with the exact command and args you specified. That process runs with the same permissions you have in your session: it can read any file you have access to, write to any directory you can write to, initiate network connections if its code decides to, and run any other program its code chooses to run. Nothing in the MCP protocol — nor in .mcp.json — restricts any of these capabilities: MCP defines how you talk to the process (JSON-RPC over stdin/stdout), not what it's allowed to do with system resources while it runs.

This isn't an MCP design flaw — it's simply what launching a subprocess means on any operating system, with or without MCP involved. reservo_full_mcp_server.py never reads any file outside itself nor makes any network connection — you know that because you wrote every line. unitconvert-mcp-server, from the previous lesson, doesn't either, in the version you saw — but you know that because you read its complete code, not because the protocol guarantees it. A third-party server you didn't read could, technically, do any of those things, and tools/list/tools/call would keep working exactly the same from outside — the protocol has no way to detect it or prevent it.


What you exactly give an http server: your data, to a remote operator

With type: "http", there's no process you launch — the server is already running, on a machine you don't control, operated by whoever published it. What you give it, in this case, is different: every request your host sends — including the arguments of every tools/call, and any header you configured, typically an authentication token — travels over the network to that remote server. That server's operator sees every request you send it, with all the data it contains. If the header includes Authorization: Bearer <your-token>, that token — and anything that token allows doing on the other end — is now something the remote operator also possesses, not just you.

The difference from stdio isn't "one is more dangerous than the other in the abstract" — lesson 04 already dismantled that false hierarchy — it's that they expose risks of a different nature: stdio puts at stake what that process can do on your machine; HTTP puts at stake what that remote operator can see of what you send it.


Worked example: naming the risk of each entry in a real .mcp.json

# trust_grant.py
"""Explains, for each .mcp.json entry, what exactly you are trusting that
server with. Does NOT implement any defense -- that is agent-security-and-
sandboxing-guide. This function only NAMES the risk based on the transport,
so the decision to register (or not) a server is made with complete
information."""
import json

with open("mcp_config_three.json", "r", encoding="utf-8") as file:
    config = json.load(file)


def describe_trust_grant(name: str, entry: dict) -> str:
    if entry["type"] == "stdio":
        return (f"{name}: LOCAL code execution, with YOUR operating-system permissions "
                f"(command: {entry['command']} {' '.join(entry['args'])}). "
                f"Can read/write any file you have access to, "
                f"make network connections if it wants to, with nothing in MCP stopping it.")
    if entry["type"] == "http":
        header_keys = list(entry.get("headers", {}).keys())
        return (f"{name}: your REQUESTS and any data you send travel to {entry['url']}, "
                f"operated by a third party. Headers sent on every call: {header_keys or 'none'}.")
    return f"{name}: unknown type, cannot describe the risk"


for server_name, entry in config["mcpServers"].items():
    print(describe_trust_grant(server_name, entry))

Against a .mcp.json with the three entries you already know from this module — Reservo (stdio), unitconvert-mcp-server (stdio), and a hypothetical reservo-cloud-mcp-server (http, from lesson 04) —:

What to expect:

reservo-mcp-server: LOCAL code execution, with YOUR operating-system permissions (command: python3.14 reservo_full_mcp_server.py). Can read/write any file you have access to, make network connections if it wants to, with nothing in MCP stopping it.
unitconvert-mcp-server: LOCAL code execution, with YOUR operating-system permissions (command: python3.14 -m unitconvert_mcp_server). Can read/write any file you have access to, make network connections if it wants to, with nothing in MCP stopping it.
reservo-cloud-mcp-server: your REQUESTS and any data you send travel to https://mcp.reservo.example.invalid/v1, operated by a third party. Headers sent on every call: ['Authorization'].

Notice describe_trust_grant applies the same risk text to reservo-mcp-server and to unitconvert-mcp-server — the function doesn't know, and has no way to know, that one is yours and the other isn't. That's, again, this module's central lesson: the structural risk (what a stdio process could do) is the same for both; what changes is how much certainty you have that, in practice, it doesn't — and that certainty comes from having read the code (Reservo) or not having read it (unitconvert-mcp-server, if it were a real package you installed without auditing).


This lesson's limit: naming, not defending

This lesson, on purpose, doesn't build any way to limit what a stdio server can do, nor to verify a remote server is who it claims to be. That — sandboxing execution (running the subprocess with restricted permissions, in a container or operating-system jail), allowlists of approved servers, granular permissions per tool, specific defenses against instruction injection via a tool's output or a resource's text, secure management of remote credentials — is the full content of agent-security-and-sandboxing-guide. This guide, and this module in particular, stop exactly at the edge: precisely naming what you're trusting an MCP server with, so you know what question to ask that guide when it's time to build the real defense.


Common mistakes

  1. Thinking MCP automatically "sandboxes" stdio servers. It doesn't, and it wasn't designed to — the protocol specifies communication (JSON-RPC over stdio/HTTP), not execution isolation. Any restriction on what a stdio process can do has to come from an external layer (the operating system, a container, a host policy) — never from the MCP protocol itself.

  2. Trusting HTTP more "because it has authentication" without thinking about what that authentication protects. A header with a token proves, to the remote server, that you are who you say you are — it says nothing about whether that remote server is honest about what it does with your data once it receives it. Authentication and operator integrity are two different questions.

  3. Applying stdio's risk to a server that actually runs remotely, or vice versa. The risk depends strictly on type, as this lesson's describe_trust_grant confirms — mixing the two leads you to worry about the wrong problem (for example, reviewing credentials for a stdio server that never makes HTTP requests, instead of reviewing which local files it could touch).

  4. Believing "I didn't read the code, but nothing weird happened either" is evidence a server is safe. It isn't — the absence of an observed incident doesn't confirm the absence of risk, it only confirms that, so far, you didn't notice it. Lesson 07 shows, run, a case where "everything works perfectly, with no protocol error at all" coexists with fully malicious intent.


Exercises

Exercise 1: Classify the risk of three scenarios (Easy)

For each one, decide whether the main risk is "local code execution" or "data exposure to a remote operator":

A) A third-party `stdio` server you installed without reading its code.
B) An `http` server that sends your complete conversation history as context on every request.
C) A `stdio` server you wrote yourself, like reservo_full_mcp_server.py.
See solution
  • A) Local code execution — the structural risk of any stdio, worsened here because you didn't audit the code: you don't know for certain what that process does with your operating-system permissions.
  • B) Data exposure to a remote operatorhttp's structural risk: any data that server receives in the request (in this case, an entire conversation history) becomes visible to whoever operates that remote server.
  • C) Local code execution, with practically zero risk in practice — it's still, structurally, a stdio process with your permissions, but since you wrote every line yourself, you have complete certainty about what it does. The structural risk still exists; what changes is your level of certainty about whether it materializes.

Exercise 2: Extend describe_trust_grant with a credentials warning (Medium)

Modify describe_trust_grant so that, when a stdio entry has a non-empty env, it adds an extra sentence warning that those environment variables — if they contain credentials — are also exposed to the process that starts up. Test it against a hypothetical entry with env: {"API_KEY": "secret-value"}.

See solution
def describe_trust_grant(name: str, entry: dict) -> str:
    if entry["type"] == "stdio":
        base = (f"{name}: LOCAL code execution, with YOUR operating-system permissions "
                f"(command: {entry['command']} {' '.join(entry['args'])}). "
                f"Can read/write any file you have access to, "
                f"make network connections if it wants to, with nothing in MCP stopping it.")
        env = entry.get("env", {})
        if env:
            base += f" IT ALSO receives these environment variables: {list(env.keys())} -- if any is a credential, this process now has it."
        return base
    if entry["type"] == "http":
        header_keys = list(entry.get("headers", {}).keys())
        return (f"{name}: your REQUESTS and any data you send travel to {entry['url']}, "
                f"operated by a third party. Headers sent on every call: {header_keys or 'none'}.")
    return f"{name}: unknown type"


print(describe_trust_grant("example-server", {
    "type": "stdio", "command": "python3.14", "args": ["server.py"],
    "env": {"API_KEY": "secret-value"},
}))

Expected output:

example-server: LOCAL code execution, with YOUR operating-system permissions (command: python3.14 server.py). Can read/write any file you have access to, make network connections if it wants to, with nothing in MCP stopping it. IT ALSO receives these environment variables: ['API_KEY'] -- if any is a credential, this process now has it.

Explanation: this exercise extends the lesson's same principle to a more specific case: env in a stdio entry doesn't just configure the process, it also hands it any value you put there — if that value is a real credential (an API key, a database token), the stdio process receives it with the same level of access you gave it for everything else. The warning doesn't prevent the risk (this lesson doesn't build defenses) — it just makes it explicit before you decide to add the entry.

Exercise 3: Design one evaluation question per transport (Hard)

Without writing code yet — just reasoning, in your own words — propose ONE concrete question you'd ask yourself before registering a new stdio server, and a DIFFERENT question you'd ask before registering a new http server, each aimed directly at that transport's specific risk (not a generic "is it safe?" question). Justify why the stdio question wouldn't work for evaluating an http server, and vice versa.

See solution

For stdio: "Did I read this server's code, or at least do I trust whoever maintains it enough not to read it myself?" — aims straight at the local code execution risk: the only way to have certainty about what a process does with your operating-system permissions is to have seen that code, or to trust the reputation of whoever wrote it.

For http: "Who do I trust to operate the server behind this URL, and what can they do with the data I send it on every request?" — aims at the data exposure risk: no matter how much you audit the protocol or the documentation, the remote operator sees every request, and no amount of code reading on your side changes that, because the code running on the other end of the URL isn't something you can read at all.

Why they're not interchangeable: the stdio question makes no sense for http, because with http there's no code you install or run locally — there's nothing to "read" on your side, the whole server lives on someone else's infrastructure. And the http question makes no sense for stdio, because a stdio server doesn't have an "operator" receiving your data in real time the same way — the risk there is what the process does on your own machine, not who you're sending information to over the network. Each transport creates a different risk surface, and the evaluation question has to target that specific surface, not a generic version of "do I trust this?"


Summary and next step

  • A stdio server receives local code execution, with your operating-system permissions — it can read/write any file you have access to, with nothing in MCP stopping it.
  • An http server receives your data — the arguments of every request, any authentication header — on every call, traveling to a process operated by someone else.
  • .mcp.json doesn't automatically distinguish between these risks — this lesson's describe_trust_grant names them with code, but doesn't prevent them: that's explicitly out of scope for this guide.
  • The real hardening — sandboxing, allowlists, granular permissions, defense against injection via tool output — is the full content of agent-security-and-sandboxing-guide. This lesson leaves the exact vocabulary to get there with the right question.

Next lesson: 07 — When a server is not trustworthy. The most specific and dangerous case of this boundary: the text a server returns — a tool's description, a resource's content — as untrusted data, run with a compromised version of unitconvert-mcp-server.


Additional resources

  1. Model Context Protocol — Specification 2025-06-18 — The base specification: defines communication, not execution isolation — the basis for why MCP doesn't "sandbox" anything on its own.
  2. Claude Code — Model Context Protocol (MCP) — How Claude Code configures credentials (env, headers) for registered servers.
  3. Python — subprocess — The real mechanism behind "local code execution": a child process with the same permissions as the parent process.
  4. Python — json — The .mcp.json reading that feeds describe_trust_grant in this lesson.