boxadm-mcp 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boxadm_mcp-0.3.2/LICENSE +21 -0
- boxadm_mcp-0.3.2/PKG-INFO +241 -0
- boxadm_mcp-0.3.2/README.md +216 -0
- boxadm_mcp-0.3.2/boxadm_mcp/__init__.py +3 -0
- boxadm_mcp-0.3.2/boxadm_mcp/__main__.py +49 -0
- boxadm_mcp-0.3.2/boxadm_mcp/client.py +470 -0
- boxadm_mcp-0.3.2/boxadm_mcp/config.py +42 -0
- boxadm_mcp-0.3.2/boxadm_mcp/oauth.py +99 -0
- boxadm_mcp-0.3.2/boxadm_mcp/server.py +626 -0
- boxadm_mcp-0.3.2/boxadm_mcp.egg-info/PKG-INFO +241 -0
- boxadm_mcp-0.3.2/boxadm_mcp.egg-info/SOURCES.txt +24 -0
- boxadm_mcp-0.3.2/boxadm_mcp.egg-info/dependency_links.txt +1 -0
- boxadm_mcp-0.3.2/boxadm_mcp.egg-info/entry_points.txt +2 -0
- boxadm_mcp-0.3.2/boxadm_mcp.egg-info/requires.txt +2 -0
- boxadm_mcp-0.3.2/boxadm_mcp.egg-info/top_level.txt +1 -0
- boxadm_mcp-0.3.2/pyproject.toml +61 -0
- boxadm_mcp-0.3.2/setup.cfg +4 -0
- boxadm_mcp-0.3.2/tests/test_access_events.py +145 -0
- boxadm_mcp-0.3.2/tests/test_client.py +90 -0
- boxadm_mcp-0.3.2/tests/test_config.py +40 -0
- boxadm_mcp-0.3.2/tests/test_daily_brief.py +87 -0
- boxadm_mcp-0.3.2/tests/test_enumeration.py +112 -0
- boxadm_mcp-0.3.2/tests/test_main.py +23 -0
- boxadm_mcp-0.3.2/tests/test_oauth.py +12 -0
- boxadm_mcp-0.3.2/tests/test_oauth_client.py +243 -0
- boxadm_mcp-0.3.2/tests/test_server.py +109 -0
boxadm_mcp-0.3.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AIKAWA Shigechika
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boxadm-mcp
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: MCP server for Box admin-log analytics — external-sharing visibility, read-only
|
|
5
|
+
Author: AIKAWA Shigechika
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shigechika/boxadm-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/shigechika/boxadm-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/shigechika/boxadm-mcp/issues
|
|
10
|
+
Keywords: box,mcp,model-context-protocol,security,audit
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Operating System :: POSIX
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp>=1.2
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
<!-- mcp-name: io.github.shigechika/boxadm-mcp -->
|
|
27
|
+
|
|
28
|
+
# boxadm-mcp
|
|
29
|
+
|
|
30
|
+
English | [日本語](README.ja.md)
|
|
31
|
+
|
|
32
|
+
MCP (Model Context Protocol) server that surfaces **external file flow** from
|
|
33
|
+
a Box admin's point of view. It reads Box's enterprise event log
|
|
34
|
+
(`admin_logs`) to highlight "who shares a lot with the outside" and "which
|
|
35
|
+
files get accessed from outside" — an early-warning signal for leakage, not a
|
|
36
|
+
general-purpose file browser.
|
|
37
|
+
|
|
38
|
+
**Read-only**: it never revokes shares, deletes files, or otherwise mutates
|
|
39
|
+
anything — it only surfaces risk. This is a different tool from a
|
|
40
|
+
general-purpose Box file MCP (the official Box MCP, or the claude.ai Box
|
|
41
|
+
connector): those operate on a user's own files and cannot see enterprise
|
|
42
|
+
events, which is exactly what this server is for.
|
|
43
|
+
|
|
44
|
+
Named after the admin-console viewpoint (`boxadm` = Box admin), sibling of
|
|
45
|
+
[`gwsadm-mcp`](https://github.com/shigechika/gwsadm-mcp).
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
| Tool | Category | Description |
|
|
50
|
+
|------|------|-------------|
|
|
51
|
+
| `health_check` | — | version + auth_mode + Box auth + `admin_logs` scope probe + configured domain allowlist. Reports `needs-login` when not yet authenticated (OAuth mode) |
|
|
52
|
+
| `recent_admin_events` | Diagnostic | Raw recent enterprise events (for checking event types/fields). Supports manual pagination via `stream_position` |
|
|
53
|
+
| `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 |
|
|
54
|
+
| `external_collaborators` | Exposure (enumeration) | Lists external collaborators (outside-org login or external invite email) |
|
|
55
|
+
| `public_shared_links` | Exposure (enumeration) | Lists items shared with an `open` (anyone-with-the-link) share link |
|
|
56
|
+
| `top_external_sharers` | Exposure (enumeration) | Ranks internal owners by external exposure (external collabs + public links) |
|
|
57
|
+
| `daily_brief` | Combined | Morning summary combining access (events) and exposure (enumeration) |
|
|
58
|
+
|
|
59
|
+
## Auth model
|
|
60
|
+
|
|
61
|
+
Two modes, selected via `BOX_AUTH_MODE`:
|
|
62
|
+
|
|
63
|
+
- `oauth` — OAuth 2.0 (user auth). An admin authorizes once in a browser; the
|
|
64
|
+
refresh token keeps it running unattended after that.
|
|
65
|
+
- `ccg` — Client Credentials Grant (server-to-server). Simpler to run
|
|
66
|
+
unattended if your Box tenant has an available server-authentication app
|
|
67
|
+
slot.
|
|
68
|
+
|
|
69
|
+
`admin_logs` (enterprise events) is readable in **either mode**, provided the
|
|
70
|
+
authorizing/impersonated user is an admin and the app has the **Manage
|
|
71
|
+
enterprise properties** scope.
|
|
72
|
+
|
|
73
|
+
### OAuth setup (one-time, by a Box admin)
|
|
74
|
+
|
|
75
|
+
1. Developer Console → Create Platform App → **Custom App → User
|
|
76
|
+
Authentication (OAuth 2.0)**
|
|
77
|
+
2. **Redirect URI**: `http://localhost:8787/callback`
|
|
78
|
+
3. **Application Scopes**: check **Manage enterprise properties** (required
|
|
79
|
+
for `admin_logs`). Add **Read all files and folders** too if you also want
|
|
80
|
+
collaboration/share-link enumeration (requires re-consent)
|
|
81
|
+
4. Enable the app in the Admin Console (unpublished apps are disabled by
|
|
82
|
+
default under most tenant policies)
|
|
83
|
+
5. Note the **Client ID / Client Secret**
|
|
84
|
+
6. First login: set `BOX_AUTH_MODE=oauth` etc., then run **`boxadm-mcp
|
|
85
|
+
auth`** → authorize in the browser → a token cache is written to
|
|
86
|
+
`~/.config/boxadm-mcp/token.json` (chmod 600)
|
|
87
|
+
|
|
88
|
+
## Setup
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# uv
|
|
92
|
+
uv pip install boxadm-mcp
|
|
93
|
+
|
|
94
|
+
# pip
|
|
95
|
+
pip install boxadm-mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or from source:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/shigechika/boxadm-mcp.git
|
|
102
|
+
cd boxadm-mcp
|
|
103
|
+
|
|
104
|
+
# uv
|
|
105
|
+
uv sync
|
|
106
|
+
|
|
107
|
+
# pip
|
|
108
|
+
pip install -e .
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
| Variable | Required | Description |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `BOX_AUTH_MODE` | | `oauth` / `ccg` (default `ccg`) |
|
|
116
|
+
| `BOX_CLIENT_ID` | ✓ | App Client ID |
|
|
117
|
+
| `BOX_CLIENT_SECRET` | ✓ | App Client Secret |
|
|
118
|
+
| `BOX_ENTERPRISE_ID` | ccg mode | Enterprise ID (CCG subject; not needed for oauth) |
|
|
119
|
+
| `BOX_OAUTH_REDIRECT_URI` | | oauth redirect. Default `http://localhost:8787/callback` |
|
|
120
|
+
| `BOX_TOKEN_CACHE` | | oauth token cache path. Default `~/.config/boxadm-mcp/token.json` |
|
|
121
|
+
| `BOX_API_BASE` | | Default `https://api.box.com` |
|
|
122
|
+
| `BOX_ALLOWED_DOMAINS` | ✓ | Internal email domains (comma-separated). No default — every address counts as external until you set this |
|
|
123
|
+
|
|
124
|
+
Keep secrets out of `.mcp.json` (e.g. in a local env file sourced before
|
|
125
|
+
launch); `.mcp.json` itself can reference `${BOX_CLIENT_ID}`-style variables
|
|
126
|
+
and be safely committed.
|
|
127
|
+
|
|
128
|
+
### Scope and limits
|
|
129
|
+
|
|
130
|
+
- **Access tools** (`external_access_events`, and the access half of
|
|
131
|
+
`daily_brief`) read the **enterprise-wide** events stream. Hitting
|
|
132
|
+
`max_events` sets `capped: true` (oldest-first scan).
|
|
133
|
+
- **Exposure (enumeration) tools** only see folders visible to the
|
|
134
|
+
co-admin account (not a guaranteed 100% of the enterprise), plus
|
|
135
|
+
`max_folders`/`max_depth` limits (surfaced via `capped`). Requires the
|
|
136
|
+
**Read all files and folders** scope.
|
|
137
|
+
- Enumeration tools share a short-TTL scan memo across calls;
|
|
138
|
+
`public_shared_links` skips collaboration calls entirely (optimization).
|
|
139
|
+
|
|
140
|
+
### DLP tracing (reverse-lookup by accessor)
|
|
141
|
+
|
|
142
|
+
To answer "what did this external account download": pass
|
|
143
|
+
`created_by_logins` (comma-separated logins) to `external_access_events`. It
|
|
144
|
+
keeps only that accessor's events and returns per-file detail
|
|
145
|
+
(`matched_events`: item id/name, owner, size in bytes+GB, timestamp,
|
|
146
|
+
event_type, whether it was via a share link).
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
external_access_events(since_hours=26, created_by_logins="someone@example.com")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- Since the accessor could appear anywhere in the window, a filtered call
|
|
153
|
+
auto-extends the scan cap to **up to 50,000 events** (oldest-first) — but
|
|
154
|
+
only matching events are kept, so memory stays bounded.
|
|
155
|
+
- In this mode the response carries `events_matched` (match count) instead of
|
|
156
|
+
`events_scanned` (no running total is kept; use `capped` to judge coverage).
|
|
157
|
+
`capped: true` means the window wasn't fully scanned — raise `max_events`.
|
|
158
|
+
- Box's `admin_logs` API has no `created_by` query parameter, so this is a
|
|
159
|
+
client-side filter (`fetch_admin_events(created_by_logins=...)`).
|
|
160
|
+
|
|
161
|
+
## Usage
|
|
162
|
+
|
|
163
|
+
### Claude Code
|
|
164
|
+
|
|
165
|
+
Add to `.mcp.json`:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"mcpServers": {
|
|
170
|
+
"boxadm-mcp": {
|
|
171
|
+
"type": "stdio",
|
|
172
|
+
"command": "boxadm-mcp",
|
|
173
|
+
"env": {
|
|
174
|
+
"BOX_AUTH_MODE": "oauth",
|
|
175
|
+
"BOX_CLIENT_ID": "${BOX_CLIENT_ID}",
|
|
176
|
+
"BOX_CLIENT_SECRET": "${BOX_CLIENT_SECRET}",
|
|
177
|
+
"BOX_ALLOWED_DOMAINS": "example.com"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### CLI Options
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
boxadm-mcp auth # OAuth first-time login (opens a browser)
|
|
188
|
+
boxadm-mcp --version # Print version and exit
|
|
189
|
+
boxadm-mcp # Start MCP server (STDIO, default)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
git clone https://github.com/shigechika/boxadm-mcp.git
|
|
196
|
+
cd boxadm-mcp
|
|
197
|
+
|
|
198
|
+
# uv
|
|
199
|
+
uv sync --dev
|
|
200
|
+
uv run pytest -v
|
|
201
|
+
uv run ruff check .
|
|
202
|
+
|
|
203
|
+
# pip
|
|
204
|
+
python3 -m venv .venv
|
|
205
|
+
.venv/bin/pip install -e . && .venv/bin/pip install pytest respx ruff
|
|
206
|
+
.venv/bin/pytest -v
|
|
207
|
+
.venv/bin/ruff check .
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Tests never touch Box — `respx` mocks the CCG/OAuth token endpoint and the
|
|
211
|
+
`admin_logs`/enumeration APIs.
|
|
212
|
+
|
|
213
|
+
## Releasing
|
|
214
|
+
|
|
215
|
+
Releases are automated with [release-please](https://github.com/googleapis/release-please).
|
|
216
|
+
Merging [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, …)
|
|
217
|
+
to `main` keeps a release PR open with the next version and changelog. Merging
|
|
218
|
+
that PR tags `vX.Y.Z` and publishes a GitHub Release, whose `release: published`
|
|
219
|
+
event triggers the `release` workflow to build and publish to PyPI and the MCP
|
|
220
|
+
Registry. release-please owns the version in `boxadm_mcp/__init__.py` and
|
|
221
|
+
`server.json` (do not bump them by hand).
|
|
222
|
+
|
|
223
|
+
> [!IMPORTANT]
|
|
224
|
+
> The release-please workflow should be given a repository secret
|
|
225
|
+
> `RELEASE_PLEASE_TOKEN` (a PAT with `contents: write` + `pull-requests: write`).
|
|
226
|
+
> The default `GITHUB_TOKEN` cannot create the Release that triggers the
|
|
227
|
+
> downstream `release` workflow (GitHub blocks workflow runs triggered by
|
|
228
|
+
> `GITHUB_TOKEN`), so without the PAT nothing gets published. The workflow falls
|
|
229
|
+
> back to `GITHUB_TOKEN` when the secret is unset so PR CI keeps working on forks.
|
|
230
|
+
|
|
231
|
+
## Governance
|
|
232
|
+
|
|
233
|
+
Because this surfaces what users share, run it as **authorized information-security
|
|
234
|
+
monitoring** with a clear purpose, a defined set of viewers, and a retention
|
|
235
|
+
policy. Most external sharing is legitimate (collaborators, vendors), so treat
|
|
236
|
+
findings as a **risk ranking**, not an alert queue — build an allowlist of
|
|
237
|
+
known-OK sharers over time.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.shigechika/boxadm-mcp -->
|
|
2
|
+
|
|
3
|
+
# boxadm-mcp
|
|
4
|
+
|
|
5
|
+
English | [日本語](README.ja.md)
|
|
6
|
+
|
|
7
|
+
MCP (Model Context Protocol) server that surfaces **external file flow** from
|
|
8
|
+
a Box admin's point of view. It reads Box's enterprise event log
|
|
9
|
+
(`admin_logs`) to highlight "who shares a lot with the outside" and "which
|
|
10
|
+
files get accessed from outside" — an early-warning signal for leakage, not a
|
|
11
|
+
general-purpose file browser.
|
|
12
|
+
|
|
13
|
+
**Read-only**: it never revokes shares, deletes files, or otherwise mutates
|
|
14
|
+
anything — it only surfaces risk. This is a different tool from a
|
|
15
|
+
general-purpose Box file MCP (the official Box MCP, or the claude.ai Box
|
|
16
|
+
connector): those operate on a user's own files and cannot see enterprise
|
|
17
|
+
events, which is exactly what this server is for.
|
|
18
|
+
|
|
19
|
+
Named after the admin-console viewpoint (`boxadm` = Box admin), sibling of
|
|
20
|
+
[`gwsadm-mcp`](https://github.com/shigechika/gwsadm-mcp).
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
| Tool | Category | Description |
|
|
25
|
+
|------|------|-------------|
|
|
26
|
+
| `health_check` | — | version + auth_mode + Box auth + `admin_logs` scope probe + configured domain allowlist. Reports `needs-login` when not yet authenticated (OAuth mode) |
|
|
27
|
+
| `recent_admin_events` | Diagnostic | Raw recent enterprise events (for checking event types/fields). Supports manual pagination via `stream_position` |
|
|
28
|
+
| `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 |
|
|
29
|
+
| `external_collaborators` | Exposure (enumeration) | Lists external collaborators (outside-org login or external invite email) |
|
|
30
|
+
| `public_shared_links` | Exposure (enumeration) | Lists items shared with an `open` (anyone-with-the-link) share link |
|
|
31
|
+
| `top_external_sharers` | Exposure (enumeration) | Ranks internal owners by external exposure (external collabs + public links) |
|
|
32
|
+
| `daily_brief` | Combined | Morning summary combining access (events) and exposure (enumeration) |
|
|
33
|
+
|
|
34
|
+
## Auth model
|
|
35
|
+
|
|
36
|
+
Two modes, selected via `BOX_AUTH_MODE`:
|
|
37
|
+
|
|
38
|
+
- `oauth` — OAuth 2.0 (user auth). An admin authorizes once in a browser; the
|
|
39
|
+
refresh token keeps it running unattended after that.
|
|
40
|
+
- `ccg` — Client Credentials Grant (server-to-server). Simpler to run
|
|
41
|
+
unattended if your Box tenant has an available server-authentication app
|
|
42
|
+
slot.
|
|
43
|
+
|
|
44
|
+
`admin_logs` (enterprise events) is readable in **either mode**, provided the
|
|
45
|
+
authorizing/impersonated user is an admin and the app has the **Manage
|
|
46
|
+
enterprise properties** scope.
|
|
47
|
+
|
|
48
|
+
### OAuth setup (one-time, by a Box admin)
|
|
49
|
+
|
|
50
|
+
1. Developer Console → Create Platform App → **Custom App → User
|
|
51
|
+
Authentication (OAuth 2.0)**
|
|
52
|
+
2. **Redirect URI**: `http://localhost:8787/callback`
|
|
53
|
+
3. **Application Scopes**: check **Manage enterprise properties** (required
|
|
54
|
+
for `admin_logs`). Add **Read all files and folders** too if you also want
|
|
55
|
+
collaboration/share-link enumeration (requires re-consent)
|
|
56
|
+
4. Enable the app in the Admin Console (unpublished apps are disabled by
|
|
57
|
+
default under most tenant policies)
|
|
58
|
+
5. Note the **Client ID / Client Secret**
|
|
59
|
+
6. First login: set `BOX_AUTH_MODE=oauth` etc., then run **`boxadm-mcp
|
|
60
|
+
auth`** → authorize in the browser → a token cache is written to
|
|
61
|
+
`~/.config/boxadm-mcp/token.json` (chmod 600)
|
|
62
|
+
|
|
63
|
+
## Setup
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# uv
|
|
67
|
+
uv pip install boxadm-mcp
|
|
68
|
+
|
|
69
|
+
# pip
|
|
70
|
+
pip install boxadm-mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or from source:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/shigechika/boxadm-mcp.git
|
|
77
|
+
cd boxadm-mcp
|
|
78
|
+
|
|
79
|
+
# uv
|
|
80
|
+
uv sync
|
|
81
|
+
|
|
82
|
+
# pip
|
|
83
|
+
pip install -e .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
| Variable | Required | Description |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `BOX_AUTH_MODE` | | `oauth` / `ccg` (default `ccg`) |
|
|
91
|
+
| `BOX_CLIENT_ID` | ✓ | App Client ID |
|
|
92
|
+
| `BOX_CLIENT_SECRET` | ✓ | App Client Secret |
|
|
93
|
+
| `BOX_ENTERPRISE_ID` | ccg mode | Enterprise ID (CCG subject; not needed for oauth) |
|
|
94
|
+
| `BOX_OAUTH_REDIRECT_URI` | | oauth redirect. Default `http://localhost:8787/callback` |
|
|
95
|
+
| `BOX_TOKEN_CACHE` | | oauth token cache path. Default `~/.config/boxadm-mcp/token.json` |
|
|
96
|
+
| `BOX_API_BASE` | | Default `https://api.box.com` |
|
|
97
|
+
| `BOX_ALLOWED_DOMAINS` | ✓ | Internal email domains (comma-separated). No default — every address counts as external until you set this |
|
|
98
|
+
|
|
99
|
+
Keep secrets out of `.mcp.json` (e.g. in a local env file sourced before
|
|
100
|
+
launch); `.mcp.json` itself can reference `${BOX_CLIENT_ID}`-style variables
|
|
101
|
+
and be safely committed.
|
|
102
|
+
|
|
103
|
+
### Scope and limits
|
|
104
|
+
|
|
105
|
+
- **Access tools** (`external_access_events`, and the access half of
|
|
106
|
+
`daily_brief`) read the **enterprise-wide** events stream. Hitting
|
|
107
|
+
`max_events` sets `capped: true` (oldest-first scan).
|
|
108
|
+
- **Exposure (enumeration) tools** only see folders visible to the
|
|
109
|
+
co-admin account (not a guaranteed 100% of the enterprise), plus
|
|
110
|
+
`max_folders`/`max_depth` limits (surfaced via `capped`). Requires the
|
|
111
|
+
**Read all files and folders** scope.
|
|
112
|
+
- Enumeration tools share a short-TTL scan memo across calls;
|
|
113
|
+
`public_shared_links` skips collaboration calls entirely (optimization).
|
|
114
|
+
|
|
115
|
+
### DLP tracing (reverse-lookup by accessor)
|
|
116
|
+
|
|
117
|
+
To answer "what did this external account download": pass
|
|
118
|
+
`created_by_logins` (comma-separated logins) to `external_access_events`. It
|
|
119
|
+
keeps only that accessor's events and returns per-file detail
|
|
120
|
+
(`matched_events`: item id/name, owner, size in bytes+GB, timestamp,
|
|
121
|
+
event_type, whether it was via a share link).
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
external_access_events(since_hours=26, created_by_logins="someone@example.com")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- Since the accessor could appear anywhere in the window, a filtered call
|
|
128
|
+
auto-extends the scan cap to **up to 50,000 events** (oldest-first) — but
|
|
129
|
+
only matching events are kept, so memory stays bounded.
|
|
130
|
+
- In this mode the response carries `events_matched` (match count) instead of
|
|
131
|
+
`events_scanned` (no running total is kept; use `capped` to judge coverage).
|
|
132
|
+
`capped: true` means the window wasn't fully scanned — raise `max_events`.
|
|
133
|
+
- Box's `admin_logs` API has no `created_by` query parameter, so this is a
|
|
134
|
+
client-side filter (`fetch_admin_events(created_by_logins=...)`).
|
|
135
|
+
|
|
136
|
+
## Usage
|
|
137
|
+
|
|
138
|
+
### Claude Code
|
|
139
|
+
|
|
140
|
+
Add to `.mcp.json`:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"mcpServers": {
|
|
145
|
+
"boxadm-mcp": {
|
|
146
|
+
"type": "stdio",
|
|
147
|
+
"command": "boxadm-mcp",
|
|
148
|
+
"env": {
|
|
149
|
+
"BOX_AUTH_MODE": "oauth",
|
|
150
|
+
"BOX_CLIENT_ID": "${BOX_CLIENT_ID}",
|
|
151
|
+
"BOX_CLIENT_SECRET": "${BOX_CLIENT_SECRET}",
|
|
152
|
+
"BOX_ALLOWED_DOMAINS": "example.com"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### CLI Options
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
boxadm-mcp auth # OAuth first-time login (opens a browser)
|
|
163
|
+
boxadm-mcp --version # Print version and exit
|
|
164
|
+
boxadm-mcp # Start MCP server (STDIO, default)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
git clone https://github.com/shigechika/boxadm-mcp.git
|
|
171
|
+
cd boxadm-mcp
|
|
172
|
+
|
|
173
|
+
# uv
|
|
174
|
+
uv sync --dev
|
|
175
|
+
uv run pytest -v
|
|
176
|
+
uv run ruff check .
|
|
177
|
+
|
|
178
|
+
# pip
|
|
179
|
+
python3 -m venv .venv
|
|
180
|
+
.venv/bin/pip install -e . && .venv/bin/pip install pytest respx ruff
|
|
181
|
+
.venv/bin/pytest -v
|
|
182
|
+
.venv/bin/ruff check .
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Tests never touch Box — `respx` mocks the CCG/OAuth token endpoint and the
|
|
186
|
+
`admin_logs`/enumeration APIs.
|
|
187
|
+
|
|
188
|
+
## Releasing
|
|
189
|
+
|
|
190
|
+
Releases are automated with [release-please](https://github.com/googleapis/release-please).
|
|
191
|
+
Merging [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, …)
|
|
192
|
+
to `main` keeps a release PR open with the next version and changelog. Merging
|
|
193
|
+
that PR tags `vX.Y.Z` and publishes a GitHub Release, whose `release: published`
|
|
194
|
+
event triggers the `release` workflow to build and publish to PyPI and the MCP
|
|
195
|
+
Registry. release-please owns the version in `boxadm_mcp/__init__.py` and
|
|
196
|
+
`server.json` (do not bump them by hand).
|
|
197
|
+
|
|
198
|
+
> [!IMPORTANT]
|
|
199
|
+
> The release-please workflow should be given a repository secret
|
|
200
|
+
> `RELEASE_PLEASE_TOKEN` (a PAT with `contents: write` + `pull-requests: write`).
|
|
201
|
+
> The default `GITHUB_TOKEN` cannot create the Release that triggers the
|
|
202
|
+
> downstream `release` workflow (GitHub blocks workflow runs triggered by
|
|
203
|
+
> `GITHUB_TOKEN`), so without the PAT nothing gets published. The workflow falls
|
|
204
|
+
> back to `GITHUB_TOKEN` when the secret is unset so PR CI keeps working on forks.
|
|
205
|
+
|
|
206
|
+
## Governance
|
|
207
|
+
|
|
208
|
+
Because this surfaces what users share, run it as **authorized information-security
|
|
209
|
+
monitoring** with a clear purpose, a defined set of viewers, and a retention
|
|
210
|
+
policy. Most external sharing is legitimate (collaborators, vendors), so treat
|
|
211
|
+
findings as a **risk ranking**, not an alert queue — build an allowlist of
|
|
212
|
+
known-OK sharers over time.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Entry point: `boxadm-mcp` (stdio server) / `boxadm-mcp auth` / `boxadm-mcp --version`."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from boxadm_mcp import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _auth() -> None:
|
|
11
|
+
"""One-time OAuth login: writes the token cache for BoxOAuthClient."""
|
|
12
|
+
from boxadm_mcp.oauth import DEFAULT_REDIRECT_URI, login
|
|
13
|
+
|
|
14
|
+
cache = login(
|
|
15
|
+
os.environ["BOX_CLIENT_ID"],
|
|
16
|
+
os.environ["BOX_CLIENT_SECRET"],
|
|
17
|
+
redirect_uri=os.environ.get("BOX_OAUTH_REDIRECT_URI", DEFAULT_REDIRECT_URI),
|
|
18
|
+
token_cache=os.environ.get("BOX_TOKEN_CACHE") or None,
|
|
19
|
+
)
|
|
20
|
+
print(f"token cache written: {cache}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
argv = sys.argv[1:]
|
|
25
|
+
if "--version" in argv:
|
|
26
|
+
print(f"boxadm-mcp {__version__}")
|
|
27
|
+
return
|
|
28
|
+
if argv and argv[0] == "auth":
|
|
29
|
+
_auth()
|
|
30
|
+
return
|
|
31
|
+
try:
|
|
32
|
+
# Import lazily so `--version` / `auth` work without the MCP runtime.
|
|
33
|
+
# The import sits inside the try so a ^C during the (slow) import chain
|
|
34
|
+
# also exits cleanly, not just one delivered while the server runs.
|
|
35
|
+
from boxadm_mcp.server import mcp
|
|
36
|
+
|
|
37
|
+
mcp.run()
|
|
38
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
39
|
+
# anyio's teardown on SIGINT dumps a 20-80 line traceback. What it
|
|
40
|
+
# raises out of mcp.run() is Python-version-dependent: a bare
|
|
41
|
+
# KeyboardInterrupt on 3.12/3.13, but asyncio.CancelledError on 3.10
|
|
42
|
+
# (asyncio.Runner.run() re-raises CancelledError instead of letting
|
|
43
|
+
# KeyboardInterrupt propagate). Catch both and exit clean, same
|
|
44
|
+
# convention as the sibling fleet MCP servers.
|
|
45
|
+
os._exit(0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
main()
|