Skip to content

Course · Claude Code · Extending Claude Code

Scope the agent's reach so a connection isn't a blank check

Suppose you have just registered a database as an MCP server - a connection that gives the agent new tools, speaking the Model Context Protocol - with a command like this one, which points the DBHub server at a payments database:

Terminal window
claude mcp add --transport stdio db -- npx -y @bytebase/dbhub \
--dsn "postgresql://readonly:pass@localhost:5432/payments"

The connection works, and that is exactly the moment to slow down. You have given an autonomous agent a live tool that runs SQL against a payments database. An MCP server is reach, and reach you did not think about is reach that surprises you later - a stray prompt, a distracted review, and the agent runs a query you would never have approved. Two questions decide how much you have actually handed over: where the connection lives and what the connection can do. (The database here is a constructed scenario; the course’s example payments repo has no database of its own. The connect lesson covers registering servers in full.)

When you run claude mcp add, you implicitly choose a scope. There are three, and the choice is about who else gets this server and whether it is checked into the repo:

  • Local (the default) - the server loads only in this project and stays private to you, stored in ~/.claude.json. Right for a personal or experimental connection, or one whose credentials must stay out of version control.
  • Project - the server is written into a .mcp.json file at the repo root, designed to be committed so every teammate gets it. Right for a connection the whole team shares.
  • User - the server loads across all your projects and stays private to you, stored in ~/.claude.json with cross-project reach. Right for a personal utility you reach for everywhere.

You set it with --scope:

Terminal window
claude mcp add --transport stdio db --scope project -- npx -y @bytebase/dbhub \
--dsn "postgresql://readonly:pass@localhost:5432/payments"

A server defined in more than one scope resolves by precedence - local over project over user - and the winning entry is taken whole; fields are not merged across scopes. The full precedence rules are on the MCP docs page.

Project scope has a safety rail worth knowing. Because a committed .mcp.json is a way for a repo to make your agent talk to its servers, Claude Code asks before first use: in an interactive session a project-scoped server appears as pending approval until you okay it, and claude mcp reset-project-choices forgets your answer. That rail is deliberate. Pulling a repo should never auto-connect your agent to someone else’s database.

Scope decides who shares the server. The connection string decides what the server can do, and that is where the real leverage sits.

Look at the database DSN in the command above. It names a readonly user:

--dsn "postgresql://readonly:pass@localhost:5432/payments"

That is not incidental. Debugging a failed charge requires reading the schema and the rows - not writing either. A connection that can only read is the difference between “the agent answered a question wrong” and “the agent dropped a table.” Hand over the narrowest credential that still does the job.

This is the same instinct as Claude Code’s permission rules, pushed down a layer - the course’s control principle, what guides execution, enforces boundaries, or allows intervention, applied at the connection as well as the machine. A permission sets what the agent may do to your machine; the credential sets what an external system will let the agent do, at the connection itself. The two layers fail independently, which is the point. The permissions chapter covers the first layer in full.

A narrow connection and a deliberate scope cover the MCP side. But the agent now has a tool, and Claude Code already has a way to govern tools: permission rules in settings.json, where an allow list names calls that run without a prompt and a deny list names calls the harness refuses outright. MCP tools are named mcp__<server>__<tool>, and that name slots straight into those rules.

Our db server makes this concrete, because DBHub’s two default tools split cleanly: search_objects discovers schema, tables, and columns, while execute_sql runs SQL - reads and writes alike. Say the team decides agents may explore the schema but never execute queries. That is a deny rule (the permissions chapter explains deny rules in depth):

.claude/settings.json
{
"permissions": {
"deny": ["mcp__db__execute_sql"]
}
}

With execute_sql denied, schema questions still route through search_objects, and the readonly DSN stays underneath: even if someone relaxes the deny rule later, the database itself still rejects writes. The connection is narrow and the permission layer is narrow, so a mistake has to get past both. That is the posture you want for any server with reach into something that matters.

Three servers arrive in one afternoon, each wanting a home. For each, pick the scope - and where a fence applies, pick that too - before reading the verdicts:

  1. DBHub against the payments Postgres - the whole team debugs failed charges with it.
  2. A notes server that fetches your personal meeting log - only you use it, in every repo you open.
  3. A teammate’s one-off helper script that happens to speak MCP and wants write access to the ledger tables.

The verdicts, explained. DBHub is shared reach into shared data: project scope, so .mcp.json carries it and the team reviews the connection string in a diff - with the readonly DSN underneath and execute_sql denied, both fences from this lesson. The notes server is personal and universal: user scope, following you across projects without ever entering a teammate’s session.

The one-off script fails before scope even enters it: an unreviewed personal script against shared money gets local scope at most - and the write access itself should not be granted under any credential; scope shares a connection, it never certifies one.

So the agent has reach, and the reach is fenced. Both fences share one property, though: they shape what the agent is allowed to do, and then trust it to operate inside the lines. For most work that is the right model. It cannot express a condition on the content of a specific action - “no change to the money-counting code without a test alongside it” is neither a credential nor a tool to forbid. That needs a check that runs against the action itself, and it is the gate we build next.