# Read-only connection snippets

Copy-ready drafts per client and vendor, checked on 2026-11-01, Google Ads, GA4, BigQuery and Claude Code permission modes again on 2026-11-06. Replace every `<...>` with a value from `martech-context/stack.md`, never with a secret. Each block changes the user's client configuration or an admin setting, so apply it only after the user, or the admin named in the heading, approves it. Confirm flags and fields against the live pages in `sources.yaml` first.

How to apply a block:

- Where the client has a command (`claude mcp add` and `claude mcp remove`, `codex mcp add` and `codex mcp remove`, `/permissions`, a connector settings page), use it.
- For a file with no command (Cursor, Claude Desktop and VS Code JSON, Codex keys such as `enabled_tools`, `.claude/settings.json` permission rules), give the user the block and the file path to paste it into.
- Never read a whole config file into the conversation: it can hold tokens. Afterwards, run `scripts/mcp_inventory.py` on every changed file.

Server names follow `<vendor>-<account>-<env>-<mode>`, for example `klaviyo-acme-prod-ro`.

## Terminal agent (Claude Code)

Personal servers go in local scope (the default) or user scope. The user completes sign-in with `/mcp` and checks the account and company shown before consenting.

```bash
# PostHog: read-only, pinned to one project, tools listed one by one (headers x-posthog-read-only, x-posthog-project-id and x-posthog-mcp-mode also work)
claude mcp add --transport http posthog-acme-prod-ro "https://mcp.posthog.com/mcp?readonly=true&project_id=<project_id>&mode=tools"

# Mixpanel, EU data residency (US: https://mcp.mixpanel.com/mcp, India: https://mcp-in.mixpanel.com/mcp)
claude mcp add --transport http mixpanel-acme-prod-ro https://mcp-eu.mixpanel.com/mcp

# Klaviyo: custom connection without write tools or customer-written-content tools
claude mcp add --transport http klaviyo-acme-prod-ro "https://mcp.klaviyo.com/mcp?read-only=true&disable-tools-with-user-generated-content=true"

# Meta Ads: your Meta app ID as the OAuth client ID; the grant (ads_mcp_management plus ads_read) is the read-only layer
claude mcp add --transport http --client-id <META_APP_ID> meta-ads-acme-prod-ro https://mcp.facebook.com/ads

# GA4: Google's local server, pinned, never started with --allow-write (put the name before -e)
claude mcp add ga4-acme-prod-ro -e GOOGLE_CLOUD_PROJECT=<gcp-project-id> -- pipx run --spec analytics-mcp==<reviewed-version> analytics-mcp
```

A shared `.mcp.json` holds only servers that are read-only by an enforced layer, with values as `${VAR}` references. Credential variables such as `ANTHROPIC_API_KEY` read as empty inside `url` and `headers`.

```json
{
  "mcpServers": {
    "posthog-acme-prod-ro": {
      "type": "http",
      "url": "https://mcp.posthog.com/mcp?readonly=true&project_id=${POSTHOG_PROJECT_ID}&mode=tools"
    }
  }
}
```

Permission rules in `.claude/settings.json`: allow the read tools the job needs, and deny write tools by verb as a backstop. Rules evaluate deny, then ask, then allow. Deny and ask rules accept globs after the server prefix, so one rule on `manage_*` also covers tools HubSpot adds later under that verb; allow globs need a literal server prefix. Read the exact tool names from `/mcp`: a connector inherited from claude.ai appears as `mcp__claude_ai_<server>__<tool>`, not under the name of a server you added.

```json
{
  "permissions": {
    "allow": [
      "mcp__hubspot-acme-prod-ro__get_user_details",
      "mcp__hubspot-acme-prod-ro__get_organization_details",
      "mcp__hubspot-acme-prod-ro__search_crm_objects",
      "mcp__hubspot-acme-prod-ro__get_crm_objects"
    ],
    "deny": [
      "mcp__hubspot-acme-prod-ro__manage_*",
      "mcp__hubspot-acme-prod-ro__submit_feedback"
    ]
  }
}
```

A deny glob only covers the verbs it names. For deny by default, run the session in `dontAsk` mode: every call that would prompt is denied and only the allow list runs, so a tool the vendor adds later stays off. Otherwise use Manual mode, where uncovered tools prompt: from v2.1.283 interactive sessions start in `auto` unless something sets a mode, so put `"permissions": {"defaultMode": "default"}` in `~/.claude/settings.json`. Keep `auto` and `bypassPermissions` off for sessions with marketing servers, and keep `enableAllProjectMcpServers` and `enabledMcpjsonServers` out of settings files in repositories that hold marketing servers (scanner S14).

## Codex

`enabled_tools` is the strongest drift control: a tool the vendor adds later stays off. Use the names the client lists, prefixes included. These keys have no command, so the user pastes the block into `~/.codex/config.toml`.

```toml
[mcp_servers.klaviyo-acme-prod-ro]
url = "https://mcp.klaviyo.com/mcp?read-only=true&disable-tools-with-user-generated-content=true"
enabled_tools = ["get_account_details", "get_campaign_report", "get_flow_report"]
default_tools_approval_mode = "writes"

[mcp_servers.google-ads-acme-prod-ro]
command = "pipx"
args = ["run", "--spec", "google-ads-mcp==<reviewed-version>", "google-ads-mcp"]
env = { GOOGLE_PROJECT_ID = "<gcp-project-id>", GOOGLE_ADS_LOGIN_CUSTOMER_ID = "<manager-id-digits-only>", GOOGLE_ADS_MCP_TOOLS_CONFIG = "<absolute-path>/tools_config.yaml" }
enabled_tools = ["customers_list_accessible_customers", "search_search", "metadata_get_resource_metadata"]
default_tools_approval_mode = "writes"
```

`GOOGLE_ADS_MCP_TOOLS_CONFIG` names a file outside the repository that turns off the server's write tools, which are on by default:

```yaml
namespaces:
  customers: true
  search: true
  metadata: true
  labels: false
  assets: false
```

## Cursor

```json
{
  "mcpServers": {
    "mixpanel-acme-prod-ro": {
      "url": "https://mcp-eu.mixpanel.com/mcp"
    }
  }
}
```

- Project file `.cursor/mcp.json`, global file `~/.cursor/mcp.json`, top-level key `mcpServers`. The user pastes the entry; the agent does not open the file.
- OAuth runs with PKCE. A vendor that needs a registered client takes an `auth` block with `CLIENT_ID` and optional `scopes`; register the redirect URL the vendor asks for (desktop `http://localhost:8787/callback`).
- Never allowlist a write tool: in Auto-review mode allowlisted MCP tools run without asking. Keep Auto-review off for sessions with write-capable marketing servers, and turn write tools off per server in the MCP settings where the client allows it.
- Prefer this native `url` entry to `npx -y mcp-remote <url>`; if a vendor guide needs the proxy, pin its version.

## VS Code

Workspace file `.vscode/mcp.json` and the user-profile `mcp.json` use the top-level key `servers`. Keys go in `inputs` with `password: true` and are referenced as `${input:<id>}`, so VS Code prompts once and stores the value securely.

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "posthog-mcp-key",
      "description": "PostHog personal API key with the MCP Server preset",
      "password": true
    }
  ],
  "servers": {
    "posthog-acme-prod-ro": {
      "type": "http",
      "url": "https://mcp.posthog.com/mcp",
      "headers": {
        "Authorization": "Bearer ${input:posthog-mcp-key}",
        "x-posthog-read-only": "true",
        "x-posthog-project-id": "<project_id>",
        "x-posthog-mcp-mode": "tools"
      }
    }
  }
}
```

Do not enable the sandbox for a write-capable marketing server: sandboxed servers auto-approve tool calls. Keep the Allow all level and the Autopilot mode off for marketing servers, and set each write tool to false in `chat.tools.eligibleForAutoApproval` so it always asks (take the tool identifiers from the live VS Code page).

## Claude Desktop and claude.ai, and ChatGPT

Klaviyo custom connector, read-only. The listed Klaviyo connector and app ignore URL parameters, so add it as a custom connection.

- Name: `klaviyo-acme-prod-ro`
- URL: `https://mcp.klaviyo.com/mcp?read-only=true&disable-tools-with-user-generated-content=true`
- Authentication: OAuth, signed in by the Klaviyo user with the Manager role
- Tool permissions after connecting: the write and delete category Blocked; the read category Needs approval or Always allow
- ChatGPT: Plugins page, add a custom MCP server with the same URL and OAuth (no Developer Mode toggle first), review the discovered tools, and keep the workspace setting for tools added later off

Salesforce custom connector on the sobject-reads server. Draft for the Salesforce admin first: activate the server in Setup under API Catalog > MCP Servers, and create an External Client App with the `mcp_api` and `refresh_token` scopes and the claude.ai callback `https://claude.ai/api/mcp/auth_callback` (a new app can take up to 30 minutes to work).

- URL, production: `https://api.salesforce.com/platform/mcp/v1/platform/sobject-reads`
- URL, sandbox or scratch org: `https://api.salesforce.com/platform/mcp/v1/sandbox/platform/sobject-reads`
- Authentication step: Use your own OAuth client; the External Client App's consumer key as the client ID; client secret blank unless the app requires one
- Never add sobject-mutations, sobject-deletes or sobject-all for a read-only pass

## Draft for the cloud admin: BigQuery deny policy

Denies Google MCP tool calls that are not marked read-only, such as BigQuery `execute_sql` and `cancel_job`, for every principal in the project. Denied tools still appear in the tool list, and the policy does not cover non-Google MCP servers. Compare with Google's page before applying; folder and organization attachment points follow the IAM deny policy overview.

```json
{
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": ["principalSet://goog/public:all"],
        "deniedPermissions": ["mcp.googleapis.com/tools.call"],
        "denialCondition": {
          "title": "Deny read-write tools",
          "expression": "api.getAttribute('mcp.googleapis.com/tool.isReadOnly', false) == false"
        }
      }
    }
  ]
}
```

```bash
gcloud iam policies create mcp-deny-policy \
  --attachment-point=cloudresourcemanager.googleapis.com/projects/<PROJECT_ID> \
  --kind=denypolicies \
  --policy-file=policy.json
```

Pair it with BigQuery Data Viewer on the approved datasets only, BigQuery Job User on the job project and MCP Tool User for the identity.

## Draft for the Snowflake admin: role, user defaults, warehouse and OAuth integration

OAuth sessions run as the connecting user's `DEFAULT_ROLE`, and a session fails to initialise when `DEFAULT_WAREHOUSE` is null, so the draft sets both. Compare every statement with Snowflake's pages before running it.

```sql
-- A small warehouse for agent queries, with a cost guard
CREATE WAREHOUSE MCP_WH WITH
  WAREHOUSE_SIZE = 'XSMALL'
  AUTO_SUSPEND = 60
  AUTO_RESUME = TRUE
  INITIALLY_SUSPENDED = TRUE
  STATEMENT_TIMEOUT_IN_SECONDS = 300;
-- Resource monitors can be created by account administrators only
CREATE RESOURCE MONITOR MCP_MONITOR WITH
  CREDIT_QUOTA = <monthly_credits>
  FREQUENCY = MONTHLY
  START_TIMESTAMP = IMMEDIATELY
  TRIGGERS ON 80 PERCENT DO NOTIFY
           ON 100 PERCENT DO SUSPEND;
ALTER WAREHOUSE MCP_WH SET RESOURCE_MONITOR = MCP_MONITOR;

-- The role the MCP connection runs as: usage plus SELECT on approved objects only
CREATE ROLE MCP_READER;
GRANT USAGE ON WAREHOUSE MCP_WH TO ROLE MCP_READER;
GRANT USAGE ON DATABASE <database> TO ROLE MCP_READER;
GRANT USAGE ON SCHEMA <database>.<schema> TO ROLE MCP_READER;
GRANT USAGE ON MCP SERVER <database>.<schema>.<mcp_server> TO ROLE MCP_READER;
GRANT SELECT ON SEMANTIC VIEW <database>.<schema>.<semantic_view> TO ROLE MCP_READER;
-- Only when the server exposes a Cortex Agent tool:
-- GRANT DATABASE ROLE SNOWFLAKE.CORTEX_AGENT_USER TO ROLE MCP_READER;
GRANT ROLE MCP_READER TO USER <pilot_user>;

-- The session runs as the default role; this changes it in every Snowflake session for this user,
-- and users can change their own default role, so prefer a dedicated MCP user per person
-- that holds MCP_READER only
ALTER USER <pilot_user> SET DEFAULT_ROLE = 'MCP_READER' DEFAULT_WAREHOUSE = 'MCP_WH';

-- Public client with PKCE; confidential clients such as Cursor or VS Code use OAUTH_CLIENT_TYPE = 'CONFIDENTIAL'
CREATE SECURITY INTEGRATION MCP_OAUTH
  TYPE = OAUTH
  OAUTH_CLIENT = CUSTOM
  ENABLED = TRUE
  OAUTH_CLIENT_TYPE = 'PUBLIC'
  OAUTH_REDIRECT_URI = 'https://claude.ai/api/mcp/auth_callback'
  OAUTH_USE_SECONDARY_ROLES = NONE
  ALLOWED_ROLES_LIST = ('MCP_READER');
```

- `ALLOWED_ROLES_LIST` only limits which roles may connect; the default role decides which one the session uses. T1 must return `MCP_READER` and `MCP_WH`.
- Keep the MCP server's SQL execution tool at `read_only: true`, which is the default, and prefer a Cortex Analyst tool over a semantic view to raw SQL.
- ChatGPT shows its redirect URI during connector creation; use that value. A client that needs several URIs uses `OAUTH_ALTERNATE_REDIRECT_URIS`.
- A network policy must allow the client provider's outbound IP ranges; when it does not, the token request fails with `invalid_client`.
- Hostnames in the server URL use hyphens, not underscores.

## Draft for the Google identity owner: GA4 credential

```bash
gcloud auth application-default login \
  --scopes https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform \
  --client-id-file=<path-outside-the-repo>/client.json
```

- The OAuth client's consent screen is Internal or published: External in Testing status issues refresh tokens that expire after 7 days.
- Application default credentials are one file per machine, and the next `application-default login` for any tool overwrites it. For a laptop that runs other Google tools, give the GA4 server its own credential file through `GOOGLE_APPLICATION_CREDENTIALS`, outside the repository with owner-only permissions, or use `--impersonate-service-account` with a read-only service account.
- `cloud-platform` is broad: record the credential as sensitive.

## Search Console community server, if the export route is not possible

```bash
uvx open-gsc-mcp@<reviewed-version> auth login --read-only --client-file <path-outside-the-repo>/client_secret.json
```

```json
{
  "mcpServers": {
    "search-console-acme-prod-ro": {
      "command": "/absolute/path/to/uvx",
      "args": ["open-gsc-mcp@<reviewed-version>"],
      "env": { "GSC_ENABLE_WRITES": "0" }
    }
  }
}
```

`GSC_ENABLE_WRITES=0` removes the sitemap and site write tools but does not narrow a token from an earlier full-scope login: revoke that grant first, then log in with `--read-only`.
