MCP is the way to give the agent what the repository itself does not hold: external tools and data over one protocol, from documentation search to queries against an internal service. The temptation is clear: add a server to the shared config, allow the agent its tools, and never return to the question. That very ease creates two quiet problems - leaked secrets and overly broad permissions - which surface later and not where you expect them.
The naive model is simple: one config, permissions granted once, and then it just works. Two expectations grow from it. The first is that a key written next to a server stays your private affair. The second is that a permission granted to a tool once is a one-off convenience with no consequences. Both expectations break on how MCP is built in Devin, and it is better to know this before the first connection than after.
The first thing that breaks the naive picture is that the config location changed. As of the Local 3.6 release (internal version v3000.3), MCP moved out of the main config into dedicated files, and prior entries from the mcpServers key are migrated into them automatically on startup. There are three files now, and each answers to its own trust boundary: the project .devin/mcp_config.json, which goes into version control and is visible to the whole team; the local override .devin/mcp_config.local.json, added to gitignore for personal and sensitive values; and the user ~/.config/devin/mcp_config.json (on Windows, under %APPDATA%), shared across all of your projects.
The split into three files is not bureaucracy but a trust boundary carried into the config. The project file answers the question "which servers does this repository need" and is therefore public by definition. The local override answers "with which keys and settings do I personally connect to them" and therefore must not leave your machine. The user file holds what is not tied to a specific project. Once these three questions are separated into three files, the question "where do I write the key" stops being a matter of guessing.
A server entry lives in the mcpServers object and comes in two kinds. A remote server is described by the url field and a transport - http or sse; headers and OAuth fields go there too when needed. A local server launches as a process: command, an args array and an env object of environment variables. Either can be turned off temporarily with the disabled field without deleting the entry. The example in this chapter shows both side by side - the remote docs and the local project-tools.
Permissions are the second half of the job, and here the principle of minimal grants applies. By default Devin Local asks for confirmation before every MCP tool call. Grant not everything at once but incrementally: first a specific tool, then, if it is called often, the whole server; first for the current session, and only then permanently. Permanent grants are conveniently kept as an explicit list in permissions.allow, where the pattern mcp__server__tool allows one tool, mcp__server__ the whole server, and mcp__ all of them. The difference between these patterns is a difference in the radius of trust, not in the convenience of writing.
Secrets call for separate discipline, because the cost of a mistake here is irreversible. Keys and tokens must not land in the project config that goes to the repository; their place is the local override or the value substitution the config supports: ${env:NAME} takes an environment variable, ${file:/path} reads a value from a file. If a remote server uses OAuth and the token has expired, the server shows a Needs auth state in the MCP list and on its card; the Authenticate button clears the stored data and runs the authorization again. That is a normal step, not an error.
The cost of neglecting permissions is not abstract and not limited to secrets. A tool that is harmless by name may return sensitive data: "read-only" describes the write right, not the harmlessness of the content. A local MCP server is a process launched on your machine with your permissions - that is, potentially arbitrary code from someone else's repository. The MCP specification treats this directly as an attack surface and requires least privilege and deliberate consent. So before granting access you check not only the tool's name but the server itself, its source, and the schema of what it returns.
You should verify the result by a method other than the one you configured with. You opened the config and wrote in a server - confirm the actual state in the interface: the Customizations panel and the MCP list show which servers are really loaded and in what status; a server in Needs auth is not connected yet. Walk through permissions.allow and make sure there is no stray mcp__* left over from debugging. Look into .local.json and the diff of the project file: not a single key should end up in what goes to the repository. These three glances take a minute and remove both quiet problems at once.
The typical failures are predictable. A key written into .devin/mcp_config.json instead of .local.json quietly leaks into git history - and you will have to revoke it as already compromised. A mcp__* grant added "so it stops asking" hands the agent the whole set of external tools indiscriminately. And the surprise "the server worked yesterday, today it asks again" is almost always explained by an expired OAuth and the Needs auth state, not by a breakage. The sign of all three is the same: access was granted wider and earlier than you understood what the server brings and where the key goes. Scope and source first - then the grant.
// .devin/mcp_config.json
{
"mcpServers": {
"docs": {
"url": "https://mcp.example.com/mcp",
"transport": "http"
},
"project-tools": {
"command": "node",
"args": ["./tools/mcp-server.js"],
"env": {}
}
}
}