Skip to content

feat: Replace --allowed-orgs gate with per-call elicitation policy for execute_anonymous #52

Description

@lukecotter

Problem

Today execute_anonymous is only registered when the server is launched with --allowed-orgs <list> (or
ALLOW_ALL_ORGS). This forces any host that wants the tool to either name every org up front or trust every
org. It's also coarse — the gate is "can the agent see the tool at all," not "can the agent run Apex against
this specific org right now."

For consumers like the Lana VS Code extension, neither default works:

  • Omitting the flag hides a useful tool from agents.
  • Setting ALLOW_ALL_ORGS permits Apex execution against any org the developer is authed to, including
    production. The realistic VS Code failure mode is users clicking "Always Allow" on the client-side per-call
    prompt, which then waives consent for prod calls too.

Proposed policy

execute_anonymous is always registered. On invocation, the server classifies the target org and applies a
per-call policy that prefers MCP elicitation when the client supports it.

Classifier

Single one-shot SOQL per org per session, cached:

SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1

Pure function: (orgRecord) => 'sandbox' | 'scratch' | 'developer' | 'production'. Cache the classification
on the server instance keyed by connection.getUsername() (or org alias if available) so a given org's type
round-trips at most once per server lifetime.

Decision tree

Applied inside the execute_anonymous handler before any code is run:

1. Classify target org (cached lookup; query if first time).
2. Classification ∈ {sandbox, scratch, developer}                    → allow.
3. Classification = production AND --allow-production-orgs set       → allow.
4. Classification = production AND client.capabilities.elicitation   → send elicitation request:
                                                                       prompt the user with the org
                                                                       alias and the Apex source.
                                                                       Allow on confirm; refuse on
                                                                       cancel/decline.
5. Classification = production, no elicitation, no flag              → refuse with documented error.

The client's elicitation capability is read from the initialize handshake
(params.capabilities.elicitation). Cache the boolean on the session once.

Elicitation request (path 4)

{
  "method": "elicitation/create",
  "params": {
    "message": "About to execute anonymous Apex against PRODUCTION org
'<alias>'.\n\nApex:\n```apex\n<source>\n```\n\nProceed?",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "confirm": { "type": "boolean", "description": "true to run, false to cancel" }
      },
      "required": ["confirm"]
    }
  }
}

If the user confirms, the tool runs as normal. If they cancel/decline (any non-confirm: true response,
including timeout), return isError: true with User declined the production-org execution.

Refusal contract (path 5 — fallback when no elicitation)

isError: true with a content[] text block:

Cannot execute anonymous Apex against production org '<alias>'.
This MCP server blocks production targets by default to prevent accidental data
loss. To enable, restart the server with --allow-production-orgs, or use a client
that supports MCP elicitation for per-call confirmation.

The error names the org, the flag, AND the alternative path — agents are good at relaying actionable text to
humans, so the message is also the docs.

CLI surface change

  • Remove --allowed-orgs (or keep as a no-op deprecation that logs a warning to stderr for one minor
    version).
  • Add --allow-production-orgs (boolean). When set, paths 4 and 5 are skipped — production behaves like
    sandbox.
  • Also consider allowed / banned lists of org with regex pattern matching which override the above behaviour.
    E.g
'allowed': ['mydevOrg@dev.com' , '.*sbx@dev.com'],
'banned': ['.*.prod@dev.com', myprodorg@dev.com]
  • We may also want a flag to force always confirm? or flags to always allow the default
    or even to disable this tool for safety?

    Versioning

    Breaking change for anyone today relying on --allowed-orgs to permit prod. Bump the major: 2.0.0.

    CHANGELOG migration:

    • --allowed-orgs <comma list> → no flag for the common case.
    • ALLOW_ALL_ORGS--allow-production-orgs only if prod targets are intentional.

    Implementation notes

    • A's refusal path and D's elicitation path share the same classifier and cache — D is purely an extra branch
      after classification returns production. No parallel pipelines.
    • execute_anonymous registration moves out of the --allowed-orgs conditional and becomes unconditional.
      Policy check lives inside the tool handler, before invoking the executeAnonymous SOAP/REST call.
    • Org SOQL should reuse the existing Connection abstraction (likely @salesforce/core Connection).
    • The elicitation handler should respect the client's response timeout but enforce a server-side max wait
      (60s) so a stuck client doesn't wedge the tool call.
      lives inside the tool handler, before invoking the executeAnonymous SOAP/REST call.
    • Org SOQL should reuse the existing Connection abstraction (likely @salesforce/core Connection).
    • The elicitation handler should respect the client's response timeout but enforce a server-side max wait (60s) so a stuck
      client doesn't wedge the tool call.

    Tests

    • Unit: classifier function — sandbox, scratch (trial date set), DE, prod, edge cases where flags are unset or
      OrganizationType is unfamiliar.
    • Unit: cache — second classification of the same org/session issues no additional SOQL.
    • Integration: client without elicitation, sandbox target → tool succeeds.
    • Integration: client without elicitation, prod target, no flag → isError: true with documented message.
    • Integration: client without elicitation, prod target, --allow-production-orgs set → tool succeeds.
    • Integration: client with elicitation, prod target, user confirms → tool succeeds.
    • Integration: client with elicitation, prod target, user cancels → isError: true "User declined…".
    • Integration: client with elicitation, prod target, --allow-production-orgs set → tool runs without prompting (flag
      short-circuits).

    Acceptance criteria

    1. Server started with no flags: tools/list includes execute_anonymous. Sandbox calls succeed silently.
    2. Prod call from a non-elicitation client → refused with the documented error.
    3. Prod call from an elicitation-capable client → user is prompted with the org alias and the Apex source; their choice is
      honored.
    4. --allow-production-orgs short-circuits both refuse and prompt paths.
    5. README: remove --allowed-orgs from setup; add a "Production safety" section explaining classification, elicitation,
      and the flag.
    6. CHANGELOG entry under 2.0.0 with the breaking-change migration.

    Out of scope

    • Per-call confirmation for non-prod targets (sandbox calls stay silent — the agent's per-call client-side gate is
      sufficient at that risk level).
    • Multi-target-org policy memory (e.g., "user already approved prod once this session" — keep it strict per-call until
      there's evidence of friction).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions