Skip to content
Who Sees What

‹ Developers

Who Sees What MCP server

Who Sees What has a hosted MCP (Model Context Protocol) server, so your own AI agent, Claude or any other MCP client, can run access audits, search for picker values, export reports, compare orgs, audit Knowledge, CMS and Experience Cloud visibility, and manage scheduled exports as tools, instead of you writing REST calls by hand. It is a Who Sees What Enterprise capability; Professional does not include MCP access.

The same bearer credential works with both this MCP server and the REST API: everything one surface can do, the other can do too, because both are generated from, and dispatch through, the same underlying contract and the same authorization, metering, and audit code. This page documents the MCP surface in full. For the REST shape of the same operations, see the API reference.

Changed November 3, 2026. This page now explains how a call works end to end, gives a connection walkthrough for every supported client, and documents every tool a credential can see, with full input and output schemas and every error.

On this page

Access tiers

MCP access is a Who Sees What Enterprise capability. A Professional org’s credential attempt is refused with a locked preview that points to the plans page. Lynceon (coming soon) is a separate Digadop product. One placeholder tool, lynceon_create_monitor, is listed to every credential today and refuses every call forbidden until Lynceon launches.

Getting a credential

There is no separate MCP signup. Create one credential in the Who Sees What web app and use it for both MCP and REST:

  1. Be on the Who Sees What Enterprise edition for the org.
  2. Sign in to the app as a user with Salesforce org-admin rights (Manage Users or Modify All Data).
  3. Open the organization menu and choose API access, then Create credential. Give it an optional label (up to 200 characters) describing what will use it, for example “Claude Desktop”.
  4. The credential (a bearer token) is shown once. Copy it immediately: Who Sees What stores only its id, key id, and label, never the token itself, so it cannot be shown again.
  5. The panel also lists your credentials (label, created time, last-used time, state) and lets you revoke one. Revoking stops it working for new calls immediately on this server instance, and within about 30 seconds everywhere.

The credential is a short-lived, ES256-signed machine token, scoped to exactly one Salesforce org and to your edition at the time it was minted. If the org’s subscription later drops below Enterprise, calls fail cleanly (see Errors below) rather than silently; mint a new credential once you are back on Enterprise.

Treat the token like a password: put it in an environment variable or your client’s secret store, never in a file you commit.

Transport and endpoint

Hosted, remote MCP over Streamable HTTP, at a single endpoint:

https://whoseeswhat.com/mcp

This is a remote HTTP server, not stdio: there is nothing to install locally, and no local process for your agent to spawn. Your agent connects directly over HTTPS.

Send your credential as a bearer token on the initialize request:

Authorization: Bearer <your-token>

Three facts about the transport decide which clients can connect, so they are worth stating plainly:

  • Streamable HTTP, JSON replies. Every request is a JSON-RPC POST to the one URL. The server answers with application/json by default; it does not require you to parse a server-sent-event stream to read a result. A GET on the same URL with a session id opens the optional server-to-client event stream, and a DELETE closes the session. Clients that only speak the older HTTP+SSE transport are not supported.
  • A static bearer header is the only credential. The server does not offer OAuth: there is no authorization endpoint, no dynamic client registration, and no .well-known metadata. A client whose “add server” screen offers only OAuth or “no authentication” cannot connect today; a client that lets you set a request header can. The client table below says which is which.
  • One session, one credential. On a successful initialize, the server returns an Mcp-Session-Id header. Send it back on every later request. The session model is below.

Session model

One MCP session is bound to exactly one credential for its whole life.

  • On a successful initialize, the server returns an Mcp-Session-Id response header. Send that same header back on every later request in the session.
  • A missing or invalid credential at initialize: HTTP 401, JSON-RPC error code -32001.
  • The identity-verification service could not answer during initialize: HTTP 503, JSON-RPC -32003, with a Retry-After header. Wait and retry the same request; do not re-authenticate.
  • A request with an unknown Mcp-Session-Id: HTTP 404, JSON-RPC -32001, message unknown MCP session.
  • A non-initialize request with no session at all: HTTP 400, JSON-RPC -32000, message no valid MCP session; send an initialize request first.
  • Session limits: a maximum of 8 concurrent sessions per credential, and 1,024 total sessions server-wide. Over either limit: HTTP 429, JSON-RPC -32002, message too many concurrent MCP sessions for this credential or server MCP session capacity reached.
  • An idle session (no request sent) is evicted after 30 minutes.
  • An unexpected server-side failure during the handshake or session handling: JSON-RPC -32603 (internal error).

These are connection-management limits, not a per-call throughput limit; see Rate limits below for the limit on how many tool calls you can make.

How a call works, end to end

Every tool on this page goes through the same pipeline. Knowing it tells you what each step can refuse, and why a refused call still costs nothing in your org.

  1. Rate limit, before anything else. The request is counted against your credential’s 120 calls per minute and the server’s 600 per minute. Over either: HTTP 429 rate_limited with a Retry-After header, and the call never reaches the MCP layer.
  2. Credential check at initialize. The bearer token’s signature, expiry, audience and org scope are verified, and the token is checked against the revocation list, before a session is opened. A refused credential is written to the audit log and answered with 401.
  3. Discovery is filtered. tools/list returns only the tools your credential is entitled to. A tool you cannot see cannot be called: the attempt fails as forbidden, exactly as if the tool did not exist.
  4. Input validation. Your arguments are validated against the tool’s published inputSchema before any code runs. Unknown fields are dropped; a missing required field or a wrong type is invalid_input.
  5. Entitlement and scope gates, in this order. The credential’s edition at mint time must be Enterprise; the orgId (or your credential’s single scoped org) must be inside the credential’s scope; then the org’s live subscription is re-read (cached for at most five minutes) and must still be Enterprise. A credential that outlives a downgrade is refused with forbidden, never silently served.
  6. The read runs as the designated user. Who Sees What holds one encrypted Salesforce session per connected org: the refresh token of the latest admin who connected it in the web app. Tool reads (SOQL, describe, Metadata and Connect REST reads) run under that user, so your results reflect that user’s view of the org and nothing is written to Salesforce, with the one exception called out in the catalog.
  7. Output validation. The response is validated on the way out against the tool’s server-side output schema, the same schema that generates the published outputSchema where one exists. The five text-only tools are validated too; their schema is simply not an object, so the catalog cannot publish it as an outputSchema. A server bug that would produce a malformed answer fails the call as upstream_error instead, and that call is not metered.
  8. Audit and usage records. Every call, refused or served, writes one audit record and one usage record in Who Sees What’s own systems. Nothing about this step touches your Salesforce org.
  9. The result. A successful call returns a text rendering, plus the result as structuredContent when the tool’s result is a JSON object (the four array-returning tools and whoseeswhat_export_report return text only; see Result shape). A refusal is a normal tool result with isError: true and a {code, message} body. See Result shape.

Connecting from each client

The server is not Claude-specific. Any MCP client that speaks Streamable HTTP and can send an Authorization header connects. Client screens change often, so the table lists the mechanism that works for each client as of November 2026, followed by a walkthrough per client. If your client’s menu differs, the three facts that never change are: the URL https://whoseeswhat.com/mcp, the transport Streamable HTTP, and the header Authorization: Bearer <your-token>.

ClientConnects today?Mechanism
Claude CodeYesclaude mcp add --transport http with a header
Claude DesktopYesclaude_desktop_config.json with the mcp-remote bridge (the Connectors screen offers OAuth only)
Claude.ai web and mobileNot yetCustom connectors there authenticate with OAuth, which this server does not offer
Claude API (Anthropic Messages API)YesThe mcp_servers connector with authorization_token
Codex CLIYes~/.codex/config.toml with a bearer token from an environment variable
ChatGPT appNot yetConnectors there authenticate with OAuth or none, with no header field
OpenAI API (Responses API, Agents SDK)YesThe hosted mcp tool with headers
CursorYesmcp.json with url and headers
WindsurfYesmcp_config.json with serverUrl and headers
VS Code (GitHub Copilot agent mode)Yes.vscode/mcp.json with type: "http", url and headers
Gemini CLIYessettings.json with httpUrl and headers
ClineYesRemote server with url and headers
Your own code (TypeScript, Python, raw HTTP)YesThe official SDKs, or three JSON-RPC requests

In every example below, <your-token> is the credential from Getting a credential, and WHOSEESWHAT_TOKEN is an environment variable holding it.

Claude Code

Claude Code registers a remote HTTP server with a header in one command:

claude mcp add --transport http whoseeswhat https://whoseeswhat.com/mcp \
  --header "Authorization: Bearer <your-token>"

Add --scope user to make it available in every project rather than the current one. Run claude mcp list to confirm whoseeswhat is registered, then start (or restart) a Claude Code session; the Who Sees What tools appear in the tool list automatically, and /mcp shows the connection state. To remove it: claude mcp remove whoseeswhat.

The same registration as a project file, .mcp.json at the project root, if you prefer to commit the shape without the token:

{
  "mcpServers": {
    "whoseeswhat": {
      "type": "http",
      "url": "https://whoseeswhat.com/mcp",
      "headers": { "Authorization": "Bearer ${WHOSEESWHAT_TOKEN}" }
    }
  }
}

Claude Desktop

Claude Desktop’s Settings, then Connectors, then Add custom connector screen takes a server URL and, optionally, OAuth client details. It has no field for a custom header, and this server authenticates with a header, so use the config file instead. Claude Desktop launches local (stdio) servers from claude_desktop_config.json; the open-source mcp-remote bridge is a tiny local process that forwards to a remote HTTP server and adds the header.

  1. Install Node.js 18 or later (the bridge runs with npx).
  2. Open Settings, then Developer, then Edit Config. That opens claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Windows: %APPDATA%\Claude\).
  3. Add the server:
{
  "mcpServers": {
    "whoseeswhat": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://whoseeswhat.com/mcp",
        "--header", "Authorization:${AUTH_HEADER}"
      ],
      "env": { "AUTH_HEADER": "Bearer <your-token>" }
    }
  }
}
  1. Quit and reopen Claude Desktop. The tools icon in a new conversation lists the whoseeswhat_ tools.

The header is passed through an environment variable, with no space after the colon, because some launch paths split a header value on spaces. If the server does not appear, open Settings, then Developer and read the server’s log; a 401 there means the token was not accepted.

Claude.ai web and mobile

Custom connectors on claude.ai (the web app and the mobile apps) authenticate with OAuth. This server has no OAuth flow, so there is nothing to enter there today. Use Claude Desktop or Claude Code, which run on your machine and can send the header. When a self-serve OAuth flow ships, this page and the REST API page will say so.

Claude API

If you are building on the Anthropic Messages API, the MCP connector lets the API call this server for you, with no client-side MCP code. Pass the server in mcp_servers with the token as authorization_token, and send the connector beta header:

import anthropic, os

client = anthropic.Anthropic()
response = client.beta.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=2000,
    messages=[{"role": "user", "content": "Which of my connected orgs can you see, and what audit questions can you run?"}],
    mcp_servers=[{
        "type": "url",
        "url": "https://whoseeswhat.com/mcp",
        "name": "whoseeswhat",
        "authorization_token": os.environ["WHOSEESWHAT_TOKEN"],
    }],
    betas=["mcp-client-2025-04-04"],
)
print(response.content)

The API performs the MCP handshake and the tool calls server-side and returns the tool results inside the response content. Only tools are supported through this path (no MCP resources or prompts), which is exactly what this server exposes.

Codex CLI

Codex reads MCP servers from ~/.codex/config.toml. A remote server takes a url, and the bearer token comes from an environment variable so it never sits in the file:

[mcp_servers.whoseeswhat]
url = "https://whoseeswhat.com/mcp"
bearer_token_env_var = "WHOSEESWHAT_TOKEN"

Export WHOSEESWHAT_TOKEN in the shell that starts Codex, then run codex mcp list to confirm the server is registered. The equivalent one-liner: codex mcp add whoseeswhat --url https://whoseeswhat.com/mcp --bearer-token-env-var WHOSEESWHAT_TOKEN. If your Codex version names the key differently (older builds used an http_headers table), the shape is the same: the URL plus an Authorization: Bearer header.

ChatGPT app

Connectors in the ChatGPT app (Settings, then Connectors, then Create, in developer mode) authenticate with OAuth or with no authentication, and have no field for a request header, so this server cannot be added there today. ChatGPT users can reach it through the OpenAI API path below, or through any of the desktop clients above.

OpenAI API

The Responses API and the Agents SDK can call a remote MCP server directly from OpenAI’s side, with the header carried in the tool definition:

from openai import OpenAI
import os

client = OpenAI()
resp = client.responses.create(
    model="gpt-5",
    input="List my connected Salesforce orgs, then tell me who can see the Acme Corp account.",
    tools=[{
        "type": "mcp",
        "server_label": "whoseeswhat",
        "server_url": "https://whoseeswhat.com/mcp",
        "headers": {"Authorization": f"Bearer {os.environ['WHOSEESWHAT_TOKEN']}"},
        "require_approval": "never",
    }],
)
print(resp.output_text)

Set require_approval to "always" (the default) if you want each tool call confirmed first; every tool on this page leaves your org unchanged except whoseeswhat_setup_auditor_permission_set, so most integrations can safely use "never" or an allow-list that excludes that one tool. In the Agents SDK the same server is a HostedMCPTool with the same tool_config object.

Cursor

Cursor reads ~/.cursor/mcp.json (global) or .cursor/mcp.json in a project. A remote server takes url and headers:

{
  "mcpServers": {
    "whoseeswhat": {
      "url": "https://whoseeswhat.com/mcp",
      "headers": { "Authorization": "Bearer <your-token>" }
    }
  }
}

Open Cursor Settings, then MCP to see the server and its tools; toggle it on if it is listed as disabled. Cursor exposes the tools to the agent in chat.

Windsurf

Windsurf reads ~/.codeium/windsurf/mcp_config.json. The remote-server key is serverUrl:

{
  "mcpServers": {
    "whoseeswhat": {
      "serverUrl": "https://whoseeswhat.com/mcp",
      "headers": { "Authorization": "Bearer <your-token>" }
    }
  }
}

Open the Cascade panel’s MCP settings and refresh; the Who Sees What tools appear under the server name.

VS Code (GitHub Copilot)

VS Code’s agent mode reads .vscode/mcp.json in the workspace (or your user settings under mcp.servers). Use an input prompt so the token is asked for once and stored in VS Code’s secret storage rather than in the file:

{
  "inputs": [
    { "type": "promptString", "id": "whoseeswhat-token", "description": "Who Sees What API credential", "password": true }
  ],
  "servers": {
    "whoseeswhat": {
      "type": "http",
      "url": "https://whoseeswhat.com/mcp",
      "headers": { "Authorization": "Bearer ${input:whoseeswhat-token}" }
    }
  }
}

Run MCP: List Servers from the command palette, start whoseeswhat, and the tools become available in Copilot Chat’s agent mode (the tools picker lets you enable or disable individual tools).

Gemini CLI

Gemini CLI reads ~/.gemini/settings.json (or .gemini/settings.json in a project). A Streamable HTTP server uses the httpUrl key:

{
  "mcpServers": {
    "whoseeswhat": {
      "httpUrl": "https://whoseeswhat.com/mcp",
      "headers": { "Authorization": "Bearer $WHOSEESWHAT_TOKEN" }
    }
  }
}

Gemini CLI expands $WHOSEESWHAT_TOKEN from your environment. Run /mcp inside the CLI to see the server’s status and tools.

Cline

In Cline’s MCP Servers panel, choose Remote Servers and add the server with its URL and the header, or edit cline_mcp_settings.json directly:

{
  "mcpServers": {
    "whoseeswhat": {
      "type": "streamableHttp",
      "url": "https://whoseeswhat.com/mcp",
      "headers": { "Authorization": "Bearer <your-token>" }
    }
  }
}

Your own code

Nothing about this server is specific to any vendor’s client. Use the official MCP SDKs or plain HTTP.

TypeScript (official SDK):

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const client = new Client({ name: 'my-agent', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(
  new URL('https://whoseeswhat.com/mcp'),
  { requestInit: { headers: { authorization: `Bearer ${process.env.WHOSEESWHAT_TOKEN}` } } },
);
await client.connect(transport);

const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));

const res = await client.callTool({
  name: 'whoseeswhat_run_template',
  arguments: { templateId: '<templateId>', orgId: '<orgId>' },
});
console.log(res.structuredContent);
await transport.terminateSession();

Python (official SDK):

import asyncio, os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    headers = {"Authorization": f"Bearer {os.environ['WHOSEESWHAT_TOKEN']}"}
    async with streamablehttp_client("https://whoseeswhat.com/mcp", headers=headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])
            orgs = await session.call_tool("whoseeswhat_list_orgs", {})
            print(orgs.structuredContent)

asyncio.run(main())

Raw HTTP (curl), the whole protocol in three requests. Useful for checking a credential from a terminal, or for a language with no SDK.

# 1. initialize: opens the session and returns Mcp-Session-Id in a response header
curl -si https://whoseeswhat.com/mcp \
  -H "Authorization: Bearer $WHOSEESWHAT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
# copy the Mcp-Session-Id header value into SID, then tell the server the handshake is done:
curl -s https://whoseeswhat.com/mcp -H "Authorization: Bearer $WHOSEESWHAT_TOKEN" -H "Mcp-Session-Id: $SID" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 2. tools/list: the entitlement-filtered catalog, with every inputSchema and outputSchema
curl -s https://whoseeswhat.com/mcp -H "Authorization: Bearer $WHOSEESWHAT_TOKEN" -H "Mcp-Session-Id: $SID" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 3. tools/call
curl -s https://whoseeswhat.com/mcp -H "Authorization: Bearer $WHOSEESWHAT_TOKEN" -H "Mcp-Session-Id: $SID" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whoseeswhat_list_orgs","arguments":{}}}'

# done: close the session (optional; idle sessions are evicted after 30 minutes)
curl -s -X DELETE https://whoseeswhat.com/mcp -H "Authorization: Bearer $WHOSEESWHAT_TOKEN" -H "Mcp-Session-Id: $SID"

Each reply is a JSON-RPC envelope: {"jsonrpc":"2.0","id":3,"result":{...}} on success, where result.structuredContent is the tool’s object and result.content[0].text is its text rendering, or result.isError: true with the {code, message} body for a refused call. Send Accept: application/json, text/event-stream on every request; the server answers in JSON.

Tool naming and discovery

Every tool name follows <product>_<endpoint> in snake_case: the product is whoseeswhat (or digadop_cms for the CMS facet, and help for the Digadop Help facet), so exportReport becomes whoseeswhat_export_report. Each tool publishes a JSON Schema inputSchema, and, when its result is a JSON object, an outputSchema describing the exact response shape, both generated mechanically from the same schema the REST endpoint uses: the request and response JSON documented in the API reference is, field for field, what each tool’s schema describes. The four tools whose result is an array (whoseeswhat_list_templates, whoseeswhat_search_users, whoseeswhat_search_objects, whoseeswhat_search_records) and whoseeswhat_export_report (whose result is one of three shapes) publish no outputSchema.

tools/list is entitlement-filtered automatically: your credential only sees the tools it is entitled to. A whoseeswhat-only credential sees the Digadop Help tools only if the org also holds a Digadop Help entitlement. The one exception to the filter is the placeholder lynceon_create_monitor, which every credential sees and no credential can call yet. Calling a tool your credential is not entitled to fails exactly as if it did not exist: forbidden.

Which org a tool acts on. Today every credential is minted for exactly one Salesforce org. The core tools take that org as an explicit orgId (get it from whoseeswhat_list_orgs); the Knowledge, CMS, Experience Cloud and Help tools take it from the credential automatically, and their optional tenantId is only consulted for a credential scoped to more than one org, which does not exist today. Leave tenantId out.

Tool catalog

A Who Sees What Enterprise credential sees 29 tools: the 28 in the four groups below, plus the placeholder lynceon_create_monitor, which refuses every call forbidden until Lynceon launches. The eight Digadop Help tools appear only with a Help entitlement.

Core audit tools (REST base /v1/whoseeswhat)

ToolREST equivalentReads / writes
whoseeswhat_list_orgsGET /orgsread
whoseeswhat_connect_orgPOST /connect/orgwrite (starts a Who Sees What connection to the org; changes no data in the org)
whoseeswhat_connect_org_statusPOST /connect/org/statuswrite (stores the Who Sees What connection once the user approves)
whoseeswhat_list_templatesGET /templatesread
whoseeswhat_run_templatePOST /templates/:templateId/runsread, metered
whoseeswhat_search_usersGET /usersread
whoseeswhat_search_objectsGET /objectsread
whoseeswhat_search_recordsGET /recordsread
whoseeswhat_export_reportPOST /reportsread, metered
whoseeswhat_list_schedulesGET /schedulesread
whoseeswhat_create_schedulePOST /scheduleswrite (a schedule record in Who Sees What, not your org)
whoseeswhat_compare_orgsPOST /compare/orgsread
whoseeswhat_setup_auditor_permission_setPOST /admin/auditor-permission-setwrites your Salesforce org’s configuration

Knowledge visibility tools (REST base /v1/digadop-knowledge): who sees which Lightning Knowledge articles.

ToolREST equivalentReads / writes
whoseeswhat_list_articlesGET /articlesread
whoseeswhat_explorer_optionsGET /explorer/optionsread
whoseeswhat_explorer_knowledgePOST /explorerread
whoseeswhat_article_audiencePOST /articles/:articleId/audienceread
whoseeswhat_category_audiencePOST /category-audienceread

CMS visibility tools (REST base /v1/digadop-cms): who sees which Salesforce CMS content, and where it is delivered.

ToolREST equivalentReads / writes
digadop_cms_list_contentGET /contentread
digadop_cms_explorer_optionsGET /explorer/optionsread
digadop_cms_content_audiencePOST /content/:contentId/audienceread
digadop_cms_explorer_cmsPOST /explorerread
digadop_cms_surface_audiencePOST /surface-audienceread

Experience Cloud visibility tools (REST base /v1/digadop-experience-cloud): what external users and the anonymous guest can see.

ToolREST equivalentReads / writes
whoseeswhat_list_membersGET /membersread
whoseeswhat_list_recordsGET /recordsread
whoseeswhat_member_visibilityPOST /members/:userId/visibilityread
whoseeswhat_guest_visibilityPOST /guest/visibilityread
whoseeswhat_record_audiencePOST /records/:recordId/audienceread

Digadop Help tools (REST base /v1/digadop-help, Help entitlement required): see Digadop Help tools.

Lynceon (coming soon) is a separate Digadop product. One placeholder tool, lynceon_create_monitor, is listed to every credential today and refuses every call forbidden until Lynceon launches.

Core audit tools

whoseeswhat_list_orgs

Purpose: list the Salesforce orgs your credential can see. Call this first to get a valid orgId for every other core tool.

How it works: reads the credential’s org scope and, for each org, whether Who Sees What holds a stored Salesforce session for it (the connected flag), its instance URL and environment. No Salesforce call is made.

Input: none.

Output:

{ "orgs": [{ "orgId": "00D000000000000EAA", "connected": true, "instanceUrl": "https://yourorg.my.salesforce.com", "environment": "production" }] }

Errors: none beyond the shared auth and rate-limit errors; this tool has no org-scope or edition check of its own.

whoseeswhat_connect_org

Purpose: start a device-code flow to connect a new Salesforce org, so your agent can drive the connection without a human first visiting the Who Sees What web app.

How it works: asks Salesforce to begin a device authorization for the chosen environment and returns the user code, the verification URL and a handle. Nothing is stored at this step; the server keeps no state between this call and the status poll, so the handle is the only thing that links them.

Input: { "environment"?: "production" | "sandbox" } (defaults to "production").

Output:

{
  "status": "awaiting_authorization", "userCode": "ABCD-1234",
  "verificationUri": "https://login.salesforce.com/setup/connect",
  "verificationUriComplete": "https://login.salesforce.com/setup/connect?user_code=ABCD-1234",
  "expiresInSeconds": 600, "pollAfterSeconds": 5, "handle": "7c0f1e2d-3b4a-4c5d-8e9f-0a1b2c3d4e5f",
  "message": "Tell the user: open https://login.salesforce.com/setup/connect and enter the code ABCD-1234 to approve. Then call whoseeswhat_connect_org_status with this handle until it reports connected."
}

status is awaiting_authorization when the flow started (only then are the code, URLs, and handle present), device_flow_disabled when the Salesforce connected app does not allow the device flow, or error; message always explains. Show the human verificationUriComplete and have them approve in a browser; a human must still complete the OAuth consent, this tool only starts that flow. Then poll with whoseeswhat_connect_org_status no faster than pollAfterSeconds.

Errors: the shared invalid_input for a bad environment value.

whoseeswhat_connect_org_status

Purpose: poll a connect attempt started by whoseeswhat_connect_org.

How it works: asks Salesforce whether the device code has been approved. On approval, Who Sees What stores the org’s refresh token, encrypted, as that org’s designated-user session, and reports whether the newly connected org is inside your credential’s scope (inCallerScope). A credential is scoped at mint time, so an org connected this way is usable from this credential only if it was already in scope; otherwise mint a credential for it.

Input: { "handle": "<string, from connect_org>" }.

Output: { "status": "pending" | "connected" | "expired" | "error", "orgId": "...", "orgName": "...", "inCallerScope": true, "message": "..." }. orgId, orgName, and inCallerScope are present on connected.

Errors: none beyond the shared ones; an unknown handle answers {"status": "error", "message": "Invalid connection handle. Start over with whoseeswhat_connect_org."}, an expired one {"status": "expired", ...}, and a declined consent {"status": "error", "message": "The user declined the connection."}, never an error result.

whoseeswhat_list_templates

Purpose: list the audit templates (questions) you can run: each one’s question pattern, access dimension, minimum tier, and the inputs (“slots”) it takes.

How it works: re-checks the live subscription of every org in the credential’s scope against the Enterprise floor and against the tier the credential was minted with, then returns the built-in template library. The catalog itself needs no Salesforce call.

Input: none.

Output: an array of:

{
  "templateId": "wsw-003", "family": "A", "questionPattern": "Who can see {target}?",
  "accessDimension": "record_read", "direction": "deny", "tier": "paid", "comingSoon": false,
  "slots": [{ "token": "target", "type": ["record"], "pickerSource": "record picker (Id or recent)", "required": true }]
}

family is one of A through G (deny diagnosis, grant diagnosis, comparison, population, privilege scan, simulation, temporal). A slot carries default only when the template defines one. tier is the minimum tier to run this template (trial/paid/pro/lynceon; pro is the internal id for Enterprise), independent of your own API-access edition.

Errors: forbidden if your org is below Enterprise, or if your credential was minted with a higher entitlement than the org currently has (message tells you to mint a new credential).

whoseeswhat_run_template

Purpose: run an audit template against a connected org and get the explained answer back in one call: a verdict, a one-line summary, the supporting evidence, and (for subject-scoped questions) the access path that produced it. This is the core operation; reports and scheduled exports both render the same evaluation.

How it works: the checks run in this order, and the first one that fails decides the answer: unknown templateId (error), edition below Enterprise (information answer), orgId outside the credential’s scope (error), the org’s live subscription no longer Enterprise (error), a required slot missing (information answer), the template’s own tier above yours (information answer), the template marked coming soon (information answer), no stored Salesforce session for the org (information answer). Only then does the evaluator run: it reads the org’s sharing settings, role hierarchy, profiles, permission sets, sharing rules, field-level security and the records the question names, all as SOQL and describe reads under the designated user’s session, and composes the verdict with its evidence. A very large comparison answer (wsw-018) is bounded to its first 500 evidence rows with exact totals in notes; whoseeswhat_export_report is the uncapped path.

Input: { "templateId": "<string>", "orgId": "<string>", "slots"?: { "<token>": "<value>" } }.

Output:

{
  "templateId": "wsw-003", "family": "A", "question": "Who can see Acme Corp (Account)?",
  "verdict": "read", "summary": "12 active users can read this record.",
  "bindingFactor": "Role: VP Sales",
  "fullPath": ["Org-Wide Default: Private", "Role Hierarchy: VP Sales to Sales Director"],
  "evidence": [{ "label": "Org-Wide Default", "value": "Private" }],
  "notes": [], "caveats": ["This answer reflects field level security at evaluation time; profile overrides after this run are not reflected."],
  "comingSoon": false
}

verdict is one of none, read, edit, full. For a population question (family D), the output adds population (a per-user array) and counts.

Important: read summary and verdict, not just whether the call succeeded. Several conditions you might expect to fail instead return a normal, successful tool result with a plain-language summary: your edition is below Enterprise, a required slot is missing, this specific template needs a higher tier, the template is comingSoon, or the org has no connected Salesforce session. Only an unknown templateId, an orgId outside your credential’s scope, or the org’s live tier dropping below Enterprise come back as a tool error (isError: true).

Errors: invalid_input (unknown templateId), forbidden (orgId out of scope, or the org’s currently-live tier is below Enterprise).

whoseeswhat_search_users

Purpose: find users in a connected org by name, to fill a {subject} slot.

How it works: one SOQL query on User under the designated user’s session: with q, Name or Username containing the text; without q, active users only. Ordered by name, 25 rows.

Input: { "orgId": "<string>", "q"?: "<free text>" }.

Output: an array of { "id": "0051a...", "name": "Jane Doe", "username": "jane@example.com", "isActive": true }, up to 25 results.

Errors: forbidden if orgId is out of scope. Returns an empty array, not an error, if the org has no connection.

whoseeswhat_search_objects

Purpose: find Salesforce objects by API name or label, to fill an object-typed slot.

How it works: one describeGlobal call, filtered in memory by a case-insensitive substring match on the API name or label, 50 rows.

Input: { "orgId": "<string>", "q"?: "<free text>" }.

Output: an array of { "apiName": "Account", "label": "Account" }, up to 50 results.

Errors: same as search users.

whoseeswhat_search_records

Purpose: find records by name within one object, to fill a {target} record slot.

How it works: the object name is checked against [A-Za-z0-9_]+ first (a SOQL-injection guard), then one SOQL query SELECT Id, Name FROM <object> with an optional Name LIKE filter, ordered by name, 25 rows.

Input: { "orgId": "<string>", "object": "<Salesforce API name>", "q"?: "<free text>" }.

Output: an array of { "id": "0011a...", "name": "Acme Corp" }, up to 25 results. Returns an empty array, not an error, if the object has no queryable Name field, or the org has no connection.

Errors: invalid_input, message invalid object name "<name>", if object is not a valid Salesforce API name. forbidden if orgId is out of scope.

whoseeswhat_export_report

Purpose: export a template’s answer as a downloadable file: CSV, XLSX, PDF, or JSON.

How it works: format is validated first, before anything else. Then the exact guard sequence of whoseeswhat_run_template runs, in the same order with the same messages, and whatever answer that sequence would have returned (a real evaluation, or one of the information answers for below-Enterprise, missing slot, below tier, coming soon or not connected) is rendered in the requested format and delivered. Under 1 MiB with delivery: "auto", the file comes back inline, base64-encoded; over 1 MiB, or with delivery: "s3", it is written to Who Sees What’s own storage and you get a signed link that expires in 15 minutes. Population answers are not capped on this path.

Input: { "templateId": "<string>", "orgId": "<string>", "slots"?: {...}, "format": "csv"|"xlsx"|"pdf"|"json", "delivery"?: "auto"|"s3" }. delivery defaults to "auto".

Output, one of three shapes:

  • Inline binary (csv/xlsx/pdf, 1 MiB or under): { "ok": true, "format": "csv", "contentType": "text/csv; charset=utf-8", "encoding": "base64", "data": "<base64>", "bytes": 4821, "correlationId": "..." }.
  • Inline JSON (format: "json"): { "ok": true, "format": "json", "report": { "...the full run_template answer..." }, "correlationId": "..." }.
  • Signed link (any format over 1 MiB, or delivery: "s3" forced): { "ok": true, "format": "pdf", "url": "https://...", "expiresAt": "<ISO8601>", "bytes": 2400000, "s3Key": "...", "correlationId": "..." }. The link expires in exactly 15 minutes (900 seconds).

Errors: invalid_input (unknown format "<value>", checked first; unknown template "<value>"), plus everything whoseeswhat_run_template can return.

whoseeswhat_list_schedules

Purpose: list the scheduled export definitions stored for an org.

How it works: edition floor, then org scope, then one read of the schedule records for that org. No Salesforce call.

Input: { "orgId": "<string>" }.

Output: { "schedules": [{ "id": "...", "templateId": "...", "slots": {...}, "format": "csv", "cron": "0 6 * * 1", "tz": "America/New_York", "sink": {"type":"email","to":"..."}, "enabled": true, "nextRunAt": "<ISO8601>", "createdAt": "<ISO8601>" }] }.

Errors: forbidden if below Enterprise (message explains the edition requirement), or if orgId is out of scope.

whoseeswhat_create_schedule

Purpose: store a scheduled export definition: a template, its slots, an output format, a cron expression with a timezone, and a destination (sink).

How it works: edition floor and org scope first, then the inputs are validated in this order: template exists, every required slot is present, the cron expression parses, the timezone is valid, the format is one of the four, and the sink is well formed (an email address, or an HTTPS webhook URL with a secret). The next run time is computed from the cron and timezone, a webhook secret is sealed with a key-management service so it is never returned by any later call, and the definition is stored. This tool stores the definition and computes nextRunAt; it does not itself run the export, and this page documents the stored contract, not when a stored schedule runs.

Input:

{
  "orgId": "00D000000000000EAA", "templateId": "wsw-003", "slots": { "target": "0051a..." },
  "format": "csv", "cron": "0 6 * * 1", "tz": "America/New_York",
  "sink": { "type": "email", "to": "security@example.com" }
}

sink is {"type":"email","to":"<address>"} or {"type":"webhook","url":"<https URL>","secret":"<plaintext HMAC secret>"}.

Output: { "schedule": { "...same shape as whoseeswhat_list_schedules' entries..." } }.

Errors (all invalid_input unless noted): unknown_template, missing_slots: <tokens>, invalid_cron, invalid_timezone, invalid_format, a specific sink-validation code such as invalid_webhook_target; upstream_error with the fixed message The upstream service failed to handle the request. if webhook sealing is temporarily unavailable server-side (the reason webhook_sink_unavailable is on the server’s audit record only); forbidden below Enterprise or if orgId is out of scope.

whoseeswhat_compare_orgs

Purpose: compare the access model of one connected org against another and summarize what was added, removed, or changed.

How it works: edition floor and orgId scope first; otherOrgId must be present and different. Then a live eligibility check: both orgs must hold active seats under the same billing account (a refused check is logged and answered forbidden). Both orgs must have a stored Salesforce session. The server then captures an access snapshot of each org (profiles, permission sets, roles, sharing settings and rules), persists both snapshots and the comparison run in Who Sees What’s own database, and returns the difference. The changes array is a bounded 50-row sample with exact counts, on this tool and on the gateway REST route alike; the full list is only in the Who Sees What web app’s comparison view.

Input: { "orgId": "<string, must be in scope>", "otherOrgId": "<string>" }.

Output:

{
  "id": "cmp_9e1f...", "otherOrgId": "00D60000000Other", "snapshotA": "...", "snapshotB": "...",
  "summary": {
    "addedCount": 3, "removedCount": 1, "changedCount": 5, "totalChanges": 9,
    "byEntryKind": { "PermissionSet": { "added": 2, "removed": 0, "changed": 1 } },
    "changes": [{ "change": "added", "entryKind": "PermissionSet", "key": "Sales_Manager", "before": null, "after": {"...":"..."} }],
    "truncated": false
  }
}

Errors: forbidden below Enterprise, out of scope, or the two orgs do not hold active seats under the same billing account (message: “This org is not eligible to compare against the requested org.”); invalid_input if otherOrgId is missing or equals orgId; forbidden if either org has no connected Salesforce session.

whoseeswhat_setup_auditor_permission_set

Purpose: a one-time (or repeatable) admin setup step: install or update a least-privilege “auditor” permission set in the connected org, optionally assigning it to an existing user. This tool writes to your Salesforce org’s configuration.

How it works: after the scope check, the permission set WSW_Auditor is created or updated through the Salesforce Metadata API under the designated user’s session (that user needs Customize Application). With assignToUser, the value is accepted as a 15- or 18-character user Id, or looked up by username, and a permission set assignment is created. The tool never creates a user. Install and assignment failures are reported in the result’s message and booleans rather than as errors, so read the output.

Input: { "orgId": "<string>", "assignToUser"?: "<existing user's Id or username>" }.

Output (always a successful result; check the booleans):

{ "permissionSet": "WSW_Auditor", "installed": true, "created": false, "assigned": true, "alreadyAssigned": false, "message": "Permission set updated and assigned to jane@example.com. Now connect Who Sees What as that user to run least-privilege." }

An org with no stored session answers installed: false with a message asking you to connect it first.

Errors: invalid_input, message No user found with username "<x>". Pass an existing user's Id or username., if assignToUser does not resolve.

Knowledge visibility tools

These five tools answer who can see Lightning Knowledge articles, in which channel, and why. They are analyses that change nothing in your org. Each one gates on the credential’s single org and the Enterprise floor, then reads the org’s Knowledge configuration directly from Salesforce (article versions, data categories, channels, profiles, permission sets, roles and the user roster, as SOQL and REST reads under the designated user’s session) and runs the visibility engine over it. Salesforce exposes no API for Data Category Visibility rules, so an answer may report the category gate as not fully decidable rather than guessing; every entry’s why chain names the gate that decided it.

Channels are InternalApp, Customer, Partner and PublicKnowledge. Gates are online, object-fls, category-visibility and channel.

whoseeswhat_list_articles

Purpose: list the org’s Knowledge articles so you can pick one for whoseeswhat_article_audience.

Input: { "tenantId"?: "<string>" } (leave out; see Which org a tool acts on).

Output: { "articles": [{ "id": "ka0...", "title": "Reset your password", "publishStatus": "Online", "channels": ["InternalApp", "Customer"] }] }. publishStatus is Draft, Online or Archived.

Errors: forbidden if the org is out of scope or below Enterprise.

whoseeswhat_explorer_options

Purpose: picker data for the Knowledge explorer: the org’s profiles, users, roles, permission sets, data categories and channels, so an agent can build a valid audience or filter for the next two tools.

Input: { "tenantId"?: "<string>" }.

Output:

{
  "profiles": [{ "id": "00e...", "label": "Standard User" }],
  "users": [{ "id": "005...", "name": "Jane Doe", "profileLabel": "Standard User" }],
  "roles": [{ "id": "00E...", "label": "VP Sales" }],
  "permissionSets": [{ "id": "0PS...", "label": "Knowledge Readers" }],
  "categories": [{ "groupName": "Products", "categoryName": "Billing", "label": "Billing" }],
  "channels": ["InternalApp", "Customer", "Partner", "PublicKnowledge"]
}

Errors: forbidden as above.

whoseeswhat_explorer_knowledge

Purpose: audience to articles: the Online articles a profile, user, role or permission set can see, optionally narrowed to one data category or channel, each with the reason it qualifies.

Input: { "audience": { "kind": "profile"|"user"|"role"|"permissionSet", "id": "<id>" }, "filter"?: { "category"?: { "groupName": "...", "categoryName": "..." }, "channel"?: "<channel>" }, "tenantId"?: "..." }.

Output: { "audience": {...}, "memberCount": 42, "items": [{ "article": { "id": "...", "title": "...", "publishStatus": "Online", "categories": [...], "channels": [...] }, "channel": "Customer", "why": [{ "gate": "channel", "code": "...", "detail": "..." }], "seenByUserIds": ["005..."] }] }. memberCount is the number of users in the audience; seenByUserIds lists which of them see the article.

Errors: invalid_input for an audience that does not match the schema; forbidden as above.

whoseeswhat_article_audience

Purpose: one article to its audience: who sees it right now, in which channel and why, plus who nearly sees it (blocked by a single gate).

Input: { "articleId": "<id>", "tenantId"?: "..." }.

Output: { "articleId": "...", "title": "...", "channels": [...], "categories": [...], "entries": [{ "user": { "id": "...", "name": "...", "profileId": "...", "roleId": null, "permissionSetIds": [...], "audience": "Customer" }, "channel": "Customer", "sees": true, "why": [...], "firstFailingGate": null }] }. For a user who does not see the article, sees is false and firstFailingGate names the gate (online, object-fls, category-visibility or channel) that stopped them.

Errors: invalid_input, message Knowledge article <id> not found in this org; forbidden as above.

whoseeswhat_category_audience

Purpose: category or channel to population: who can read Knowledge articles in a data category or channel, segmented by the path that grants it, plus the Online articles in scope. The public-exposure auditor’s view: ask for channel: "PublicKnowledge" to see what the internet can read.

Input: { "category"?: { "groupName": "...", "categoryName": "..." }, "channel"?: "<channel>", "tenantId"?: "..." } (give at least one of category or channel).

Output: { "scope": { "category"?: {...}, "channel"?: "..." }, "population": [{ "via": { "kind": "role", "roleId": "..." } | { "kind": "permissionSet", "permissionSetId": "..." } | { "kind": "group-default", "groupName": "..." } | { "kind": "channel", "channel": "..." }, "grant": "...", "users": [...] }], "articlesInScope": [{ "id": "...", "title": "...", "publishStatus": "Online", "categories": [...], "channels": [...] }] }.

Errors: forbidden as above.

CMS visibility tools

These five tools answer who sees Salesforce CMS (and Enhanced CMS) content, and where it is delivered. They are analyses that change nothing in your org, with the same gate and read path as the Knowledge tools: the org’s CMS workspaces, content, channels, sites, pages, audience segments and roster are read from Salesforce (SOQL and Connect REST) under the designated user’s session, and the visibility engine evaluates five gates in order: publish, channel, access, placement, audience. Experience Builder audiences, pages and placements have no query API, so a verdict can be reportable (targeted, but not fully decidable) instead of yes or no; the contract carries that honestly rather than guessing.

A subject is one of { "kind": "guest", "siteId" } (the anonymous visitor of a site), { "kind": "audience", "audienceId" }, { "kind": "profile", "profileId" } or { "kind": "user", "userId" }. A placement is { "channelId", "siteId", "pageId", "componentPlacementId", "audienceId" } with nulls where a level does not apply.

digadop_cms_list_content

Purpose: list the org’s CMS managed content so you can pick an item for digadop_cms_content_audience.

Input: { "tenantId"?: "..." }.

Output: { "content": [{ "id": "20Y...", "title": "Spring promotion", "workspaceId": "0Zu..." }] }.

Errors: forbidden if the org is out of scope or below Enterprise.

digadop_cms_explorer_options

Purpose: picker data for the CMS explorer: channels, sites, pages, audience segments, profiles and users.

Input: { "tenantId"?: "..." }.

Output: { "channels": [{ "id", "label" }], "sites": [{ "id", "label" }], "pages": [{ "id", "label", "siteId" }], "audiences": [{ "id", "label", "siteId" }], "profiles": [{ "id", "label" }], "users": [{ "id", "name", "profileLabel" }] }.

Errors: forbidden as above.

digadop_cms_content_audience

Purpose: one content item to its audience: every delivery placement (channel, site, page) where it renders, the subjects that see it there and why, with public deliveries flagged.

Input: { "contentId": "<id>", "tenantId"?: "..." }.

Output: { "contentId": "...", "deliveries": [{ "placement": {...}, "publicAccess": true, "subjects": [{ "subject": {...}, "sees": "yes"|"no"|"reportable", "why": [{ "gate": "audience", "decision": "yes", "code": "...", "detail": "..." }] }] }] }. publicAccess: true means the anonymous guest of that site sees the content at that placement.

Errors: invalid_input, message CMS content <id> not found in this org; forbidden as above.

digadop_cms_explorer_cms

Purpose: subject to content: the content a guest, audience segment, profile or user sees, optionally narrowed to one channel, site or page, each with the reason.

Input: { "subject": <subject>, "filter"?: { "channelId"?: "...", "siteId"?: "...", "pageId"?: "..." }, "tenantId"?: "..." }.

Output: { "subject": {...}, "filter": {...}, "items": [{ "contentId": "...", "title": "...", "placement": {...}, "sees": "yes"|"no"|"reportable", "why": [...] }] }.

Errors: invalid_input for a subject that does not match one of the four shapes; forbidden as above.

digadop_cms_surface_audience

Purpose: surface to audience: audit a channel or a site (or drill to one page): what content renders there, who sees each placement, and how much of it the anonymous guest gets.

Input: { "filter": { "channelId"?: "...", "siteId"?: "...", "pageId"?: "..." }, "tenantId"?: "..." }.

Output: { "filter": {...}, "placements": [{ "placement": {...}, "content": {...}, "publicAccess": true, "subjects": [...] }], "guestSummary": { "siteId": "...", "seesCount": 7, "totalContent": 12 } }. guestSummary is null when the filter names no site.

Errors: forbidden as above.

Experience Cloud visibility tools

These five tools answer what external (community, portal) users and the anonymous guest can see, record by record. They are analyses that change nothing in your org. After the same gate as the other facets, the org’s sharing configuration is read from Salesforce with SOQL only (sharing sets, share groups, the external role rollup, external org-wide defaults and guest sharing rules), and the engine computes each grant with its why-chain. Four mechanisms are not evaluated in this version and are named on every answer in notEvaluated (manual-share, apex-managed-share, criteria-based-sharing-rule, experience-builder-audience), so the absence of a grant is never presented as proof of no access. Access levels are None, Read and ReadWrite.

An org with no Experience Cloud site answers invalid_input with a message saying so, not a server error.

whoseeswhat_list_members

Purpose: list the site’s external users so you can pick one for whoseeswhat_member_visibility.

Input: { "tenantId"?: "..." }.

Output: { "members": [{ "id": "005...", "name": "Pat Customer", "accountId": "001..." }] }.

Errors: invalid_input if the org has no Experience site; forbidden if out of scope or below Enterprise.

whoseeswhat_list_records

Purpose: list the records in the modeled scope, optionally for one object, so you can pick one for whoseeswhat_record_audience.

Input: { "object"?: "<Salesforce API name>", "tenantId"?: "..." }.

Output: { "records": [{ "id": "001...", "object": "Account", "label": "Acme Corp" }] }.

Errors: as whoseeswhat_list_members.

whoseeswhat_member_visibility

Purpose: what one authenticated external user sees: every record visible to them through the evaluated grants, each with its access level and why, plus whether they passed the site-membership gate.

Input: { "userId": "<external user Id>", "tenantId"?: "..." }.

Output: { "subject": { "type": "external-user", "userId": "...", "name": "..." }, "hasSiteMembership": true, "records": [{ "record": { "id", "object", "ownerId", "accountId"? }, "access": "Read", "why": [{ "code": "...", "detail": "..." }] }], "notEvaluated": ["manual-share", "apex-managed-share", "criteria-based-sharing-rule", "experience-builder-audience"] }.

Errors: invalid_input, message External user <id> not found in this org's Experience site; the no-site and shared errors above.

whoseeswhat_guest_visibility

Purpose: what the anonymous guest sees: the records exposed to the internet through guest sharing rules, each with why. The public-exposure headline view.

Input: { "tenantId"?: "..." }.

Output: the same shape as whoseeswhat_member_visibility, with "subject": { "type": "guest" } and hasSiteMembership always true.

Errors: the no-site and shared errors above.

whoseeswhat_record_audience

Purpose: who sees one record: its guest verdict and its external audience, with access levels and why-chains. The leak audit for a single record.

Input: { "recordId": "<record Id>", "tenantId"?: "..." }.

Output:

{
  "record": { "id": "001...", "object": "Account", "ownerId": "005...", "accountId": "001..." },
  "guest": { "visible": false, "exposedToInternet": false, "access": "None", "why": [], "uncertain": false },
  "external": [{ "subjectType": "external-user", "subjectId": "005...", "subjectName": "Pat Customer", "accountId": "001...", "access": "Read", "why": [{ "code": "...", "detail": "..." }] }],
  "notEvaluated": ["manual-share", "apex-managed-share", "criteria-based-sharing-rule", "experience-builder-audience"]
}

guest.uncertain is true when secure-guest-user enforcement could not be modeled conclusively; treat exposedToInternet as a lower bound in that case. subjectType is external-user or internal-share-group.

Errors: invalid_input, message Record <id> is not in the modeled Experience Cloud scope; the no-site and shared errors above.

Digadop Help tools

A credential whose org also holds a Digadop Help entitlement sees eight more tools on this same server, under the help_ prefix (REST base /v1/digadop-help). They answer who sees which Help content in your org, and let an agent ask the Help assistant a product question. They are listed here so your tools/list has no surprises; a credential without the Help entitlement does not see them.

ToolREST equivalentWhat it does
help_list_contentGET /contentLists the org’s published Help content (id, title, surface).
help_list_layoutsGET /layoutsLists the org’s Help page layouts (id, name, label, surface, active).
help_explorer_optionsGET /explorer/optionsPicker data: targeted objects and record types, plus the profile and user roster.
help_analyze_content_audiencePOST /content/:contentId/audienceWho sees one Help item right now, per user and per circumstance, with humanized reasons.
help_analyze_layout_audiencePOST /layouts/:layoutId/audienceWho sees any content a layout places, with the circumstances.
help_help_for_context_by_profilePOST /explorer/help-for-profileEvery Help item a profile sees under a context filter (object, record type, page, application, UI type).
help_help_for_context_by_userPOST /explorer/help-for-userThe same, for one user (resolved to their profile).
help_assistant_askPOST /assistant/askAsks the Help assistant a question; returns kind (answered, clarifying, abstained and so on), text and citations.

None of the eight changes your org. Their full schemas are published in tools/list like every other tool.

Result shape and error handling

A successful tool call returns a normal MCP tool result: the resource as text content, and, for every tool whose result is a JSON object, also as structuredContent matching the tool’s outputSchema. The four array-returning tools (whoseeswhat_list_templates, whoseeswhat_search_users, whoseeswhat_search_objects, whoseeswhat_search_records) and whoseeswhat_export_report return text content only: parse the JSON from the text.

A rejected call (not entitled, bad input, an upstream failure) comes back as a normal MCP tool result with isError: true, carrying the same {code, message} shape documented per tool above and in the API reference’s error table. This is a tool-level result, not a JSON-RPC protocol error. The only JSON-RPC-level errors are the handshake and session-management failures described under Session model (-32000 through -32003, -32603; -32003 is an identity-verification outage on the initialize preflight, answered HTTP 503 with a Retry-After header: retry, do not re-authenticate): everything else surfaces as isError: true on an otherwise normal tool call, so write your agent’s error handling to check isError and error.code inside every tool result, not just for a transport failure.

The error codes are: unauthorized, forbidden, invalid_input, not_found, upstream_error, rate_limited, and auth_unavailable (retryable; the identity-verification service could not answer, do not re-authenticate, just retry).

Output validation

Every tool’s response, including the five text-only tools that publish no outputSchema, is validated server-side against its output schema before it is returned to you. Where the catalog publishes an outputSchema, it is generated from that same schema. If a server bug would have produced a response that violates the documented schema, the call fails cleanly as upstream_error instead of a wrong or malformed answer reaching your agent (and it is not metered or billed). This means you can trust that a successful response always matches the shapes documented on this page.

Correlation IDs

The server tracks a correlation id per call for logging and support. Send one yourself as an x-correlation-id request header (8 to 128 characters of letters, digits, ., _, : or -) and the server honors it and echoes it back on the response; otherwise it mints one and returns it in the same header. It is also a field on whoseeswhat_export_report’s response body (correlationId). Quote that value to support if you need help with a specific call.

Rate limits

Every call through this MCP server is subject to the same rate limit as the REST API: 120 requests per minute per credential, with a global backstop of 600 requests per minute across all callers. Exceeding it fails the request with HTTP 429 and the code rate_limited, with a Retry-After header saying how long to wait. This is separate from the session caps under Session model above, which govern how many concurrent sessions you can hold open, not how many tool calls you can make per minute.

How access and safety work

  • whoseeswhat_setup_auditor_permission_set is the setup tool that writes to your org: it writes the named permission set and, if asked, assigns it to an existing user.
  • Who Sees What keeps its own records of what you ran: every call, including a refused one, adds an audit record and a usage record in Who Sees What’s own systems. An export delivered as a link stores the file in Who Sees What’s own storage, a comparison saves the two access snapshots it compared, and a schedule stores its definition. None of this changes your Salesforce org.
  • Your credential’s entitlement snapshot is re-checked against your org’s current, live subscription on every call, so a credential minted while you were on Enterprise stops working cleanly (not silently) once the subscription is no longer Enterprise.
  • Discovery (tools/list) is entitlement-filtered: a tool you are not entitled to is neither listed nor callable, and attempting it anyway fails identically to it not existing.

First prompts to try

Once a client is connected, these prompts exercise the main paths. Each one maps to the tools named in brackets.

  • “Which Salesforce orgs can you see?” [whoseeswhat_list_orgs]
  • “What audit questions can you run, and which ones need inputs?” [whoseeswhat_list_templates]
  • “Who can see the Acme Corp account?” [whoseeswhat_search_records, then whoseeswhat_run_template]
  • “Export that answer as a PDF.” [whoseeswhat_export_report]
  • “Which Knowledge articles can the public read?” [whoseeswhat_category_audience with channel: "PublicKnowledge"]
  • “What can an anonymous visitor to our Experience site see?” [whoseeswhat_guest_visibility]
  • “Where is the Spring promotion content published, and who sees it?” [digadop_cms_list_content, then digadop_cms_content_audience]
  • “Compare our sandbox’s access model with production.” [whoseeswhat_compare_orgs]

Questions about the MCP server? Contact us, or read the REST API reference.