CLAUDE LABJP
MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructureEXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioningADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applicationsQUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the windowPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days outFIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attributionMCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructureEXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioningADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applicationsQUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the windowPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days outFIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
Articles/Claude Code
Claude Code/2026-07-27Advanced

Whose Environment Expands That Variable? Fingerprinting Your Effective Managed MCP Policy

Variable references in the Managed MCP allowlist and denylist now resolve from the startup environment and the managed-settings env block. I rebuilt both resolution orders locally to see where verdicts diverge, then wrote a preflight check that reduces the effective policy to a comparable fingerprint.

Claude Code225MCP51permissions6automation101

Premium Article

The change was a single line: variable references in the Managed MCP allowlist and denylist now resolve from the startup environment and the managed-settings env block, instead of the env block inside the settings file itself.

Reading it stopped me. As an indie developer I run my own scheduled automation, and it is written on the assumption that environment variables do not carry between invocations. The startup environment is, for me, the least stable input there is. If that becomes the resolution source, then the meaning of a policy can shift from one run to the next.

An allowlist only buys you confidence if what you wrote is what gets enforced. When that depends on the environment, reading the file confirms nothing. So I rebuilt both resolution orders locally and measured exactly where the verdicts diverge.

Resolution moved from inside the file to outside it

Under the previous behavior, a variable reference resolved from the settings file's own env block. The world was closed: read the distributed JSON and you could read the expanded values too.

Now the startup environment is consulted first, then the managed-settings env. The intent makes sense — you can swap values between CI and a workstation without editing the file. But the inputs that decide the policy have moved outside the administrator's reach.

The official documentation is explicit about the shape of this. Both serverCommand and serverUrl go through the same expansion on the policy side and on the server-config side before matching. And it warns directly: because expansion reads Claude Code's own process environment, a policy entry that references a variable expands to whatever value the user sets, so entries you rely on for enforcement should be written as literals.

Obvious once stated. What surprised me when I measured it was how far that one sentence reaches.

The harness

The actual expansion lives inside Claude Code, so I rebuilt it locally following the documented rules. The expander below takes resolution sources as an ordered array, so switching between the old and new behavior is just reordering that array.

// expand.mjs — reproduce ${VAR} / ${VAR:-default} with a swappable resolution order
const REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
 
// sources: array of [name, map]; earlier entries win
export function expand(value, sources, opts = {}) {
  const misses = [];
  const out = String(value).replace(REF, (whole, name, dflt) => {
    for (const [origin, map] of sources) {
      if (map && Object.prototype.hasOwnProperty.call(map, name) && map[name] !== undefined) {
        return String(map[name]);
      }
    }
    if (dflt !== undefined) { misses.push({ name, kind: "default", used: dflt }); return dflt; }
    misses.push({ name, kind: "unresolved", used: "" });
    return "";
  });
  return { out, misses };
}
 
export function expandEntry(entry, sources, opts) {
  const misses = [];
  const push = r => { misses.push(...r.misses); return r.out; };
  if (entry.serverUrl !== undefined)
    return { entry: { serverUrl: push(expand(entry.serverUrl, sources, opts)) }, misses };
  if (entry.serverCommand !== undefined)
    return { entry: { serverCommand: entry.serverCommand.map(a => push(expand(a, sources, opts))) }, misses };
  return { entry: { ...entry }, misses }; // serverName never expands
}

The matcher follows the documented rules as written: URLs accept * anywhere, hostnames are case-insensitive and ignore a trailing FQDN dot, paths stay case-sensitive, and a pattern with no path matches any path. Commands must match exactly, argument for argument.

// match.mjs — URL wildcard matching, exact command matching, and evaluation order
function normHost(h) { return h.replace(/\.$/, "").toLowerCase(); }
 
function splitUrl(u) {
  const m = /^([^:]*):\/\/([^/?#]*)(.*)$/.exec(u);
  return m ? { scheme: m[1], host: m[2], path: m[3] } : null;
}
 
function globToRe(s, { ci = false } = {}) {
  const body = s.split("*").map(p => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
  return new RegExp("^" + body + "$", ci ? "i" : "");
}
 
export function urlMatches(pattern, url) {
  const p = splitUrl(pattern), u = splitUrl(url);
  if (!p || !u) return globToRe(pattern, { ci: true }).test(url);
  if (!globToRe(p.scheme, { ci: true }).test(u.scheme)) return false;
  if (!globToRe(normHost(p.host), { ci: true }).test(normHost(u.host))) return false;
  if (p.path === "") return true;                 // a pattern with no path matches any path
  return globToRe(p.path).test(u.path === "" ? "/" : u.path);
}
 
export function commandMatches(pattern, cmd) {
  return Array.isArray(pattern) && Array.isArray(cmd)
    && pattern.length === cmd.length
    && pattern.every((a, i) => a === cmd[i]);
}
 
function hit(entry, server) {
  if (entry.serverUrl !== undefined)
    return server.type === "remote" && urlMatches(entry.serverUrl, server.url);
  if (entry.serverCommand !== undefined)
    return server.type === "stdio" && commandMatches(entry.serverCommand, server.command);
  if (entry.serverName !== undefined) return entry.serverName === server.name;
  return false;
}
 
// allow === undefined means unrestricted; [] means nothing allowed
export function evaluate(server, { allow, deny = [] }) {
  for (const e of deny) if (hit(e, server)) return { allowed: false, reason: "denylist" };
  if (allow === undefined) return { allowed: true, reason: "no-allowlist" };
  const hasUrl = allow.some(e => e.serverUrl !== undefined);
  const hasCmd = allow.some(e => e.serverCommand !== undefined);
  for (const e of allow) {
    if (!hit(e, server)) continue;
    if (e.serverName !== undefined) {
      if (server.type === "remote" && hasUrl) continue;   // name matches don't count once URL entries exist
      if (server.type === "stdio" && hasCmd) continue;
    }
    return { allowed: true, reason: Object.keys(e)[0] };
  }
  return { allowed: false, reason: "not-in-allowlist" };
}

Leaving serverName unexpanded is per spec, but implementing it made the reason concrete. A name is a label the user assigns, not the server itself, and the documentation says plainly that it is not a security control. If you want enforcement, write a command or a URL.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A working harness (expander, matcher, differ) that reproduces how one managed-settings.json turns into different policies per environment, shown as a seven-server verdict table
Measured asymmetry of unresolved variables: the same collapsed pattern matches 4 of 4 URLs under one host-normalization rule and 0 of 4 under the other, so an allowlist over-tightens while a denylist silently opens
Full source of a preflight script that reduces the effective policy to a sha256 fingerprint, runs in 42ms, and exits non-zero on unresolved or empty expansions
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Claude Code2026-05-04
Claude Code MCP Servers in Practice — From Setup to Real-World Use
A practical guide to connecting MCP servers in Claude Code — covering configuration tips, common errors, and real project setups that actually work.
Claude Code2026-03-20
Implementation Patterns for Custom Claude Code Skills — SKILL.md Design, Testing, and Distribution
A hands-on tutorial for building three production custom Claude Code skills from scratch. Covers SKILL.md structure, agent types, context injection, testing, and team distribution.
Claude Code2026-08-10
The Same rm -rf Was Recoverable in Ten Places and Unrecoverable in Five — Measuring Reversibility Before Auto Mode Becomes the Default
Auto mode becomes the default on Pro, Max and Team from August 14. It stops on operations judged irreversible, destructive, or outward-facing — but reversibility turned out to be a property of state, not of commands. Here is the probe and the measurements.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →