# MCP security review: exampleco-crm for the weekly paid and pipeline summary

Fictional example. Acme Co, Example Co, every host and every value here are invented. Roles only, no names.

## Decision

- Outcome: GO WITH CONDITIONS (final)
- Valid until: 2027-04-30 or the first re-review trigger, whichever comes first
- Reason in one sentence: the vendor-hosted server is identified and the CRM role removes its outward leg, but unannotated tools, client-side write blocks, a Codex entry outside the review, a missing log and unproven unattended controls are still open.
- What would change a NO-GO: not applicable
- Tier: 3, set by: write tools are reachable (manage_records, manage_segments, create_note, publish_page, draft_marketing_email) and search_contacts returns personal data
- Reviewer: marketing ops lead; system owner: CRM admin; privacy owner: data protection lead; connection owner: marketing ops lead
- Review date: 2026-10-30; decided by: CRM admin, with the data protection lead for the data path

## Scope

- Use case: a weekly paid performance and pipeline summary for the Monday marketing meeting
- Data categories: deal records (personal: yes, owner names), company names (personal: no), pipeline aggregates (personal: no), marketing email aggregates (personal: no)
- Identity: per-user OAuth, custom View-only role, read from get_user_details and the CRM admin's user screen on 2026-10-29
- Clients and plans: AI chat app, business plan, desktop app on three laptops. Codex is out of scope: one marketer's Codex config also connects exampleco_crm, and that entry comes out before connecting (C8 below).
- Mode: attended
- Server: vendor-hosted remote
- Endpoint: https://mcp.example.com/crm
- Repository and tag: not applicable (vendor-hosted, closed source)
- Capture: example-exampleco-crm-tools.json, source live capture, captured 2026-10-28, server version 2026.10 as reported, complete yes (no nextCursor), matches the team's client yes (the AI chat app's connector tool settings listed the same 12 tools for the test identity on 2026-10-29)
- List fingerprint: sha256:57005035c7308975a1cf14787434ec9e8c5741dd7f20a46a63c792ba3e5be7a5
- Instructions fingerprint: sha256:478ff8ad4205407c173d80c67e7c6a24fc136f93e7c2560a1319064452b9f790

## Blockers

- None open.
- B4 removed: every read tool returns private data (personal data in search_contacts and get_contact), list_notes returns notes from web forms written by outsiders, and publish_page is outward, all on one server. Enforcing control: the View-only role has no content publish permission; publish_page was denied in the sandbox on 2026-10-29 at 10:42 UTC. The client blocks on publish_page and list_notes are defence in depth: the role still allows list_notes, so the outsider-text leg is only blocked client-side.
- B3 not raised: the role denies manage_segments and publish_page (sandbox tests below), so both hard-to-reverse tools are held back below the prompt.

## Conditions

- C3 Missing annotations: get_email_performance and draft_marketing_email carry no hints. get_email_performance prompts on every call; draft_marketing_email is blocked under C5; no approval mode that trusts read-only marks. Due: before connecting. Owner: marketing ops lead. Closing evidence: export of the connector's tool settings. Status: open
- C5 Reversible writes reachable but not needed: block manage_records, manage_segments, create_note, publish_page and draft_marketing_email in the org connector settings. The role still allows create_note, so that boundary is client-side only. Due: before connecting. Owner: CRM admin. Closing evidence: the connector's tool settings showing each one blocked. Status: open
- C8 Unreviewed static token (Codex path): one marketer's Codex config connects exampleco_crm with bearer_token_env_var EXAMPLECO_CRM_TOKEN, a static bearer token whose type, creator, role and storage this review did not see; a shell profile export would be plaintext, and a token created by an admin could carry more than the View-only role. Remove the entry from that config before connecting; adding Codex later is a new client and needs its own scope line (token type and role from get_user_details, storage, enabled_tools and prompt). Due: before connecting. Owner: marketing ops lead. Closing evidence: a rescan of that Codex config without exampleco_crm. Status: open
- C10 Logging gap: enable client telemetry with tool names and find a test call in the vendor's connected-app log. Due: within 30 days, 2026-11-29. Owner: marketing ops lead. Closing evidence: the log entry for a test call with user, tool and time. Status: open
- C15 Attended use only: no scheduled or research-mode runs with this connector, and no automatic approval mode. Due: before connecting. Owner: marketing ops lead. Closing evidence: research mode off for the connector in the org settings, no scheduled tasks. Status: open
- C6 Personal data beyond the task: met on 2026-10-29. The role hides contact email and phone (a test search_contacts at 11:05 UTC returned id, first_name, last_name, company and lifecycle_stage only), the client blocks search_contacts, get_contact and list_notes, which the task does not need (the intake excludes free text), and the privacy owner signed off in D-20261029-1.
- C11 Revocation untested: met on 2026-10-29 at 14:10 UTC. The CRM admin revoked the test user's grant in the CRM's connected apps settings and the next get_deal_pipeline_summary call failed with an authorization error.

## Checks

### Publisher and supply chain

- Vendor page naming this endpoint: Example Co's remote MCP developer page, https://developers.example.com/mcp, read 2026-10-28
- Registry entry: com.example/crm (reverse-DNS namespace), pointing at the same endpoint
- Package: not applicable
- Pin: not possible for a remote server; drift checks every 30 days instead
- Advisory check: GitHub Advisory Database and the vendor's security page on 2026-10-28; nothing for this server
- Hosted third party: no; the vendor runs the server in the account's region (US)

### Tools and definitions

- Script: `node scripts/review-mcp-tools.mjs assets/example-exampleco-crm-tools.json`; summary: 1 FAIL, 1 WARN, 16 INFO, 6 PASS
- Session run with adsbridge-mcp, rejected in its own record: 6 FAIL, 4 WARN, 30 INFO, 6 PASS
- Counts: 7 read, 3 reversible write, 2 hard-to-reverse
- Hard-to-reverse tools: manage_segments, publish_page
- Indicators and verdicts:
  - S01 trifecta on this server alone: confirmed; handled by the role (B4 above)
  - T02 draft_marketing_email and get_email_performance without hints: confirmed; C3
  - T10 search_contacts returns email, first_name and last_name: confirmed; C6
  - Server instructions, read in full: benign; they name only this server's get_user_details
- Annotations trusted: yes for the annotated tools on this version, which matched their names and sandbox behaviour; unannotated tools are treated as destructive

### Permissions and read-only enforcement

- Permission level at the source: custom View-only role, from get_user_details and the CRM admin's user screen, 2026-10-29
- Consent scopes: the screen listed read and write access to contacts, companies, deals, notes, segments, pages and marketing email drafts. Scopes follow the tools at install, so the role is the boundary, not the grant.
- Enforcing layer: source permission (View-only role) for publish_page, manage_segments and manage_records; client block only for create_note and draft_marketing_email
- Tests in the Example Co CRM sandbox:
  - Allowed read: 10:31 UTC, get_deal_pipeline_summary for Q4 matched the pipeline report in the UI
  - Out-of-scope read: 10:35 UTC, search_deals for deal D-TEST-0042 in the partner business unit, which the CRM admin confirmed exists from the admin view, returned no results
  - Harmless writes: 10:42 publish_page on a test page, denied; 10:44 manage_segments add_members on a test segment, denied; 10:46 manage_records update of a test deal description, denied; 10:48 create_note on a test contact, allowed (the CRM admin deleted the note afterwards)
  - Second identity: 10:55 UTC, a second marketer's get_deal_pipeline_summary returned the same totals
- Production identities: the three marketers' production users hold the custom View-only role, read from the CRM admin's user screen on 2026-10-30 (read-only)

### Data path and personal data

- Deal records (personal: yes, owner names): name, stage, amount, close date, company name, owner name, then Example Co's hosted server (US), then the AI provider on the business plan (training off by default per the plan terms, retention per the provider's data page, both cited 2026-10-29), then chat history in the AI app workspace (30-day workspace setting) and the desktop app on three laptops, then the vendor's connected-app log (user, tool, time), then the weekly summary in the internal marketing doc
- Pipeline and email aggregates (personal: no): the same path
- Contacts: not needed; email and phone hidden by the role, contact tools blocked in the client
- Notes (personal: yes, free text from web forms): not needed and out of scope; list_notes is blocked in the client, a client-side boundary because the role still allows it, so no free text reaches the AI provider and the free-text sign-off is not required
- Minimisation: role field permissions plus client blocks, shown by the 11:05 UTC test query
- Privacy owner sign-off: decisions.md D-20261029-1

### Outsider text and session design

- Servers in the session: exampleco-crm only. adsbridge-mcp was rejected; the official Google Ads server, with its write categories off, gets its own Tier 1 record before it joins and adds no outward tool.
- Legs: private data, all seven read tools (personal data in search_contacts and get_contact); outsider text list_notes; outward publish_page
- Removed leg: outward, enforced by the View-only role, proven by the denied publish_page call at 10:42 UTC. The client block on list_notes narrows the outsider-text leg but does not remove it.
- Canary cases, kept as defence in depth while list_notes is only blocked client-side, run by the marketing ops lead in the AI chat app on the business plan with the team's instructions: crm-note on test contact C-TEST-0001, 11:20 UTC, pass; hidden-markup on test company ACC-TEST-0002, 11:32 UTC, pass. The task reads no analytics dimensions, so utm-or-page-title and search-term do not apply.

### Tokens, client settings and logging

- Config scan: `node scripts/scan-mcp-config.mjs assets/example-acme-mcp-config.json assets/example-acme-codex-config.toml`; summary: 5 FAIL, 7 WARN, 8 INFO, 2 PASS across 9 servers in 2 files. exampleco-crm passes in both files. Every failing entry belongs to adsbridge or to five older servers; they were removed from the config the marketers receive and their scan lines went to their owners.
- Token storage: per-user OAuth in the AI app's managed connector store. The marketer's Codex config names EXAMPLECO_CRM_TOKEN, a static token outside this review; the entry comes out before connecting (C8)
- Approval setting per write tool, once C3 and C5 close: manage_records, manage_segments, create_note, publish_page and draft_marketing_email blocked; get_email_performance prompt; search_contacts, get_contact and list_notes blocked (C6); the other read tools allowed
- Session permission mode per client: AI chat app: per-tool connector settings, set in the org connector settings, research mode off under C15 and no automatic approval mode
- Log outside the agent: the vendor's connected-app log; no test call seen yet (C10)
- Revocation: owner CRM admin; where: the CRM's connected apps settings per user; tested 2026-10-29 at 14:10 UTC (C11)

## Not checked

- Server source code: closed source; behaviour was tested instead.
- Meta Ads reporting: deferred to its own review; no Meta test ad account yet.
- The AI provider's sub-processors: the privacy owner's area, cited in D-20261029-1.
- Codex as a client: out of scope; its exampleco_crm entry is removed under C8.

## Re-review

- Triggers: a list or instructions fingerprint change, new scopes or a re-install prompt, a role change, a new client or plan, a new data category (contacts), users beyond the three marketers, a vendor advisory.
- Drift check: every 30 days, next on 2026-11-29: `node scripts/review-mcp-tools.mjs <new capture> --baseline mcp-reviews/baselines/exampleco-crm-2026.10-2026-10-28-tools.json`; any D01 to D05 finding is a re-review trigger
- Full re-review: 2027-04-30 (Tier 3, every 6 months)

## Proposed martech-context updates (not applied)

- stack.md, Example Co CRM: Agent access: exampleco-crm remote MCP at https://mcp.example.com/crm, View-only role, write tools blocked in the connector settings; Credentials: per-user OAuth in the AI app's managed store, no shared secret
- decisions.md: D-20261030-1, exampleco-crm GO WITH CONDITIONS for the weekly summary, Tier 3; C3, C5, C8 and C15 before connecting (marketing ops lead, CRM admin); C10 by 2026-11-29 (marketing ops lead); valid until 2027-04-30 or the first trigger
- mcp-reviews/2026-10-30-exampleco-crm.md: this record
- mcp-reviews/baselines/exampleco-crm-2026.10-2026-10-28-tools.json: the capture
- README.md (index): under Optional files present, add mcp-reviews/, written by mcp-security-review: one record per server and use case, baselines in mcp-reviews/baselines/
