Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.9.1 instead.
Reason given by maintainers: broken

boxadm-mcp

English | 日本語

MCP (Model Context Protocol) server that surfaces external file flow from a Box admin's point of view. It reads Box's enterprise event log (admin_logs) to highlight "who shares a lot with the outside" and "which files get accessed from outside" — an early-warning signal for leakage, not a general-purpose file browser.

Read-only: it never revokes shares, deletes files, or otherwise mutates anything — it only surfaces risk. This is a different tool from a general-purpose Box file MCP (the official Box MCP, or the claude.ai Box connector): those operate on a user's own files and cannot see enterprise events, which is exactly what this server is for.

Named after the admin-console viewpoint (boxadm = Box admin), sibling of gwsadm-mcp.

Features

Tool Category Description
health_check version + auth_mode + Box auth + admin_logs scope probe + configured domain allowlist. Reports needs-login when not yet authenticated (OAuth mode)
recent_admin_events Diagnostic Raw recent enterprise events (for checking event types/fields). Supports manual pagination via stream_position
external_access_events Access (events, enterprise-wide) Aggregates external DOWNLOAD/PREVIEW within a window: top external accessors, top externally-accessed files, share-link count. Pass created_by_logins for DLP tracing of a specific account
external_collaborators Exposure (enumeration) Lists external collaborators (outside-org login or external invite email)
public_shared_links Exposure (enumeration) Lists items shared with an open (anyone-with-the-link) share link
top_external_sharers Exposure (enumeration) Ranks internal owners by external exposure (external collabs + public links)
get_user Account state (lookup) One account by its exact login: status, role, enterprise, quota, timestamps. Answers "is this account disabled?" without an admin console
daily_brief Combined Morning summary combining access (events) and exposure (enumeration)

Auth model

Two modes, selected via BOX_AUTH_MODE:

  • oauth — OAuth 2.0 (user auth). An admin authorizes once in a browser; the refresh token keeps it running unattended after that.
  • ccg — Client Credentials Grant (server-to-server). Simpler to run unattended if your Box tenant has an available server-authentication app slot.

admin_logs (enterprise events) is readable in either mode, provided the authorizing/impersonated user is an admin and the app has the Manage enterprise properties scope.

OAuth setup (one-time, by a Box admin)

  1. Developer Console → Create Platform App → Custom App → User Authentication (OAuth 2.0)
  2. Redirect URI: http://localhost:8787/callback
  3. Application Scopes: check Manage enterprise properties (required for admin_logs). Add Read all files and folders too if you also want collaboration/share-link enumeration (requires re-consent)
  4. Enable the app in the Admin Console (unpublished apps are disabled by default under most tenant policies)
  5. Note the Client ID / Client Secret
  6. First login: set BOX_AUTH_MODE=oauth etc., then run boxadm-mcp auth → authorize in the browser → a token cache is written to ~/.config/boxadm-mcp/token.json (chmod 600)

Setup

# uv
uv pip install boxadm-mcp

# pip
pip install boxadm-mcp

Or from source:

git clone https://github.com/shigechika/boxadm-mcp.git
cd boxadm-mcp

# uv
uv sync

# pip
pip install -e .

Configuration

Variable Required Description
BOX_AUTH_MODE oauth / ccg (default ccg). Any other value falls back to ccg; health_check reports the mode in effect, so a typo shows up there as ccg rather than as what was typed
BOX_CLIENT_ID App Client ID
BOX_CLIENT_SECRET App Client Secret
BOX_ENTERPRISE_ID ccg mode Enterprise ID (CCG subject; not needed for oauth)
BOX_OAUTH_REDIRECT_URI oauth redirect. Default http://localhost:8787/callback
BOX_TOKEN_CACHE oauth token cache path. Default ~/.config/boxadm-mcp/token.json
BOX_API_BASE Default https://api.box.com
BOX_SCAN_CONCURRENCY Parallel per-folder lookups in the enumeration scan. Default 8, clamped 132
BOX_SCAN_DEADLINE Soft wall-clock budget (seconds) for one enumeration scan. Default 45; 0/negative disables it. When hit, the scan returns a disclosed partial (capped=true) instead of running until the tool call times out
BOX_HTTP_TIMEOUT Per-request HTTP timeout (seconds). Default 30. Lower it (with BOX_SCAN_DEADLINE) so one slow endpoint can't stretch the final in-flight scan batch past a gateway timeout
BOX_ALLOWED_DOMAINS Internal email domains (comma-separated). No default — every address counts as external until you set this

Keep secrets out of .mcp.json (e.g. in a local env file sourced before launch); .mcp.json itself can reference ${BOX_CLIENT_ID}-style variables and be safely committed.

Scope and limits

  • Access tools (external_access_events, and the access half of daily_brief) read the enterprise-wide events stream. Hitting max_events sets capped: true (oldest-first scan).
  • Exposure (enumeration) tools only see folders visible to the co-admin account (not a guaranteed 100% of the enterprise), plus max_folders/max_depth limits (surfaced via capped). Requires the Read all files and folders scope.
  • The scan fans its per-folder lookups out concurrently (BOX_SCAN_CONCURRENCY), since Box has no enterprise-wide collaboration listing — this widens how many folders finish inside a tool-call timeout, but coverage is still bounded by the caps. The read path retries 429 (honoring Retry-After) and transient 5xx with jittered backoff, so a passing throttle recovers instead of degrading coverage; a folder dropped by a per-folder API error that outlasts those retries (e.g. a persistent 403) is counted in fetch_errors: coverage is complete only when capped is false and fetch_errors is 0.
  • Enumeration tools share a short-TTL scan memo across calls; public_shared_links skips collaboration calls entirely (optimization).
  • get_user reads the enterprise user directory instead — one request, no paging, and structurally not an enumerator (it answers about the login you pass and nothing else). Its capped flag discloses a truncated search, so a found: false from a truncated result reads as inconclusive rather than negative.

DLP tracing (reverse-lookup by accessor)

To answer "what did this external account download": pass created_by_logins (comma-separated logins) to external_access_events. It keeps only that accessor's events and returns per-file detail (matched_events: item id/name, owner, size in bytes+GB, timestamp, event_type, whether it was via a share link).

external_access_events(since_hours=26, created_by_logins="someone@example.com")
  • Since the accessor could appear anywhere in the window, a filtered call auto-extends the scan cap to up to 50,000 events (oldest-first) — but only matching events are kept, so memory stays bounded.
  • In this mode the response carries events_matched (match count) instead of events_scanned (no running total is kept; use capped to judge coverage). capped: true means the window wasn't fully scanned — raise max_events.
  • Box's admin_logs API has no created_by query parameter, so this is a client-side filter (fetch_admin_events(created_by_logins=...)).

Per-account lookup (get_user)

Every other tool reads the event stream or walks folders, so an account with no recent activity cannot be asked about at all. get_user answers directly — "is this account disabled, and is its quota full?" — in one request:

get_user(login="someone@example.com")

login is the account's full Box login (an email address), matched exactly and case-insensitively. That matching is the point, not an implementation detail: Box's underlying filter_term is a prefix search over display name and login, so the endpoint readily returns a colleague whose name starts with the same letters. Only an exact login match lands in user; everything else is counted in other_prefix_hits and never identified. A term that is not email-shaped is refused before the request is made — filter_term has no minimum length, so a one-character term would otherwise return a page of real accounts.

One drift it cannot find: the same person under a second login at another domain. filter_term prefix-matches the whole term, so alice@old.example can never return alice@new.example; that would need a search on the local part alone, which is the directory-wide prefix search this tool refuses by design.

Field Meaning
found The only field that says whether the account exists. false is a normal answer, not an error
user The account when found, else null: status, role, enterprise, space_used / space_amount, created_at, modified_at
other_prefix_hits Count of further prefix matches. A count only: those are different accounts and are deliberately not identified
capped The search was truncated, so found: false is inconclusive rather than negative
search_hits, note How many entries came back, and a plain-language reading

[!NOTE] Whether /2.0/users is reachable has not been verified end-to-end in either auth mode, so this README deliberately claims no scope requirement for it. Under oauth the effective permission is the authorising user's. A permission failure returns likely_cause naming the two things to check — that user's Box role, and the app's Application Scopes — rather than a bare HTTP status.

Usage

Claude Code

Add to .mcp.json:

{
  "mcpServers": {
    "boxadm-mcp": {
      "type": "stdio",
      "command": "boxadm-mcp",
      "env": {
        "BOX_AUTH_MODE": "oauth",
        "BOX_CLIENT_ID": "${BOX_CLIENT_ID}",
        "BOX_CLIENT_SECRET": "${BOX_CLIENT_SECRET}",
        "BOX_ALLOWED_DOMAINS": "example.com"
      }
    }
  }
}

CLI Options

boxadm-mcp auth       # OAuth first-time login (opens a browser)
boxadm-mcp --version  # Print version and exit
boxadm-mcp            # Start MCP server (STDIO, default)

Development

git clone https://github.com/shigechika/boxadm-mcp.git
cd boxadm-mcp

# uv
uv sync --dev
uv run pytest -v
uv run ruff check .

# pip
python3 -m venv .venv
.venv/bin/pip install -e . && .venv/bin/pip install pytest respx ruff
.venv/bin/pytest -v
.venv/bin/ruff check .

Tests never touch Box — respx mocks the CCG/OAuth token endpoint and the admin_logs/enumeration APIs.

Live smoke test

That isolation is the point of the unit tests, and also their limit: they cannot tell you that a tool has stopped returning real data. scripts/smoke_test.py runs every registered tool against the configured enterprise and fails on empty, malformed or error answers:

# needs the same BOX_* environment variables as the server
uv run python scripts/smoke_test.py
uv run python scripts/smoke_test.py --only shared_links --traceback
  • Read-only. Every tool here reads; nothing in Box is changed. A future tool that writes must be listed as state-changing and skipped, and a test enforces that.
  • No payloads in the report. Tool names, statuses and row counts only; server-authored error text is redacted too, since Box errors quote the account or item they were asked about.
  • Bounded. These tools page the event stream and walk the folder tree, so each probe passes explicit small caps instead of the interactive defaults (5000 events, 150 folders) — enforced by a test that finds the bounding parameters from the source.
  • Nothing enterprise-specific in the specs. A test bans address shapes (login, URL, hostname, IPv4, IPv6) and the parameters that carry an account name, because this repository is public. Two literals identify nobody and are allowed: folder id 0, the root folder in every enterprise, and the made-up term get_user is probed with — an account that cannot exist, so the probe asserts the not-found path rather than naming a real person.
  • An empty answer passes: no public links and no external collaborators is the desired state, so probes assert the accounting envelope (count, folders_scanned, window_hours) rather than a row count.
  • CI enforces the cheap half: a tool registered without a probe spec fails the build (tests/test_smoke_probes.py), so adding a tool forces the question "how would we know it works?".
  • scripts/smoke_harness.py is the engine and holds no Box knowledge: it is kept identical across the servers that share it, so fix engine bugs once and sync the file rather than patching this copy.

Releasing

Releases are automated with release-please. Merging Conventional Commits (feat:, fix:, …) to main keeps a release PR open with the next version and changelog. Merging that PR tags vX.Y.Z and publishes a GitHub Release, whose release: published event triggers the release workflow to build and publish to PyPI and the MCP Registry. release-please owns the version in boxadm_mcp/__init__.py and server.json (do not bump them by hand).

[!IMPORTANT] The release-please workflow should be given a repository secret RELEASE_PLEASE_TOKEN (a PAT with contents: write + pull-requests: write). The default GITHUB_TOKEN cannot create the Release that triggers the downstream release workflow (GitHub blocks workflow runs triggered by GITHUB_TOKEN), so without the PAT nothing gets published. The workflow falls back to GITHUB_TOKEN when the secret is unset so PR CI keeps working on forks.

Governance

Because this surfaces what users share, run it as authorized information-security monitoring with a clear purpose, a defined set of viewers, and a retention policy. Most external sharing is legitimate (collaborators, vendors), so treat findings as a risk ranking, not an alert queue — build an allowlist of known-OK sharers over time.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

boxadm_mcp-0.7.0.tar.gz (69.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

boxadm_mcp-0.7.0-py3-none-any.whl (38.6 kB view details)

Uploaded Python 3

File details

Details for the file boxadm_mcp-0.7.0.tar.gz.

File metadata

  • Download URL: boxadm_mcp-0.7.0.tar.gz
  • Upload date:
  • Size: 69.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for boxadm_mcp-0.7.0.tar.gz
Algorithm Hash digest
SHA256 9ddce492ae9b75ef661484c477770503fb2fb6a8d616ad7bf64b1c14e4a3d741
MD5 6f71e892e581752d04afac63e9a1feae
BLAKE2b-256 1d095922ad75238fb9b83cdf5eee0df80e67963e1baa48f7bdf0e39c9690e863

See more details on using hashes here.

Provenance

The following attestation bundles were made for boxadm_mcp-0.7.0.tar.gz:

Publisher: release.yml on shigechika/boxadm-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file boxadm_mcp-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: boxadm_mcp-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 38.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for boxadm_mcp-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 438191f11622b09c98d2c37e33c02db5d4a6a39f6bd56d291842890afe2c1eab
MD5 224891c404edc5474fcbda8a4552f342
BLAKE2b-256 bc9c64132afcfe2b641de2cecabc89416196145edf2d5577dadbb3b79bd1febf

See more details on using hashes here.

Provenance

The following attestation bundles were made for boxadm_mcp-0.7.0-py3-none-any.whl:

Publisher: release.yml on shigechika/boxadm-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.1

2 files

This release

0.7.0 This release

2 files

0.6.1

2 files

0.6.0

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.7

2 files

0.3.6

2 files

0.3.5

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page