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.
@@ -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,3 @@
1
+ """boxadm-mcp — Box admin-log analytics MCP (read-only external-sharing visibility)."""
2
+
3
+ __version__ = "0.3.2" # x-release-please-version
@@ -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()