worksmobile-mcp
# worksmobile-mcp
**Unofficial** NAVER WORKS (LINE WORKS) **Drive admin** CLI + MCP server, built on the
Developer API's **service-account delegation** (JWT `delegated_user`).
Existing WORKS MCP servers focus on the *end-user* plane (your own mail/calendar/files via
User OAuth). This project covers the *admin* plane that they don't:
- act **as any member** of your tenant via delegation (like Google domain-wide delegation)
- **shared-drive governance**: list drives, inspect `accessibleRange`/`permissionType`,
grant/revoke drive- and folder-level permissions, toggle folder inheritance (`enable`/`disable`)
- **share-create** ("shared with me" shortcuts), My Drive scaffolding, upload/download, search
- **directory audit**: account-hygiene rules and reconciliation against an external roster β
find ex-employees who still hold live mail/drive access
> Not affiliated with NAVER / WORKS MOBILE Corp. "NAVER WORKS", "LINE WORKS" and
> worksmobile.com are their trademarks/properties. Official API docs:
> <https://developers.worksmobile.com/>
> ### π Just want to use WORKS yourself? This is the wrong tool.
>
> If you want an AI agent to read *your own* mail, calendar, drive or tasks, **stop here** β
> see **[docs/END_USER_SETUP.md](docs/END_USER_SETUP.md)**. That path takes minutes, needs no
> admin rights, and only ever touches your own account.
>
> This project is for **tenant administrators**. It authenticates with a delegated service
> account that can act as *any member of the organization*, which is why it needs credentials
> a normal member cannot get. If your Developer Console says *"you do not have access"*,
> that is not a setup problem to solve β it is the boundary working as intended.
## Safety design
> ### π Read this before you run it anywhere but your own machine
>
> This is not a per-user integration. The credential it takes is a **delegated service
> account that can act as any member of your tenant** β read anyone's drive, change anyone's
> sharing. Treat the private key like a domain-admin key, because that is what it is.
>
> **Do not paste these credentials into third-party MCP hosting, an "install/deploy this
> server" button, or a hosted inspector**, however convenient. Those run the server on
> someone else's machine, and handing them this key hands over the whole tenant. Directory
> and marketplace listings of this repo are automated crawls, not endorsements of any
> deployment model β the only configuration this project supports is **local stdio, on the
> administrator's own machine**.
>
> If you genuinely need the HTTP transport, bind it to localhost and put real
> authentication in front of it. Key compromise here is not recoverable by rotating a
> password: until the key is revoked in the Developer Console, the holder is every employee.
WORKS Drive permission APIs have sharp edges. This tool encodes them:
- **Every mutating operation requires explicit confirmation** β CLI: `--yes`
(non-interactive runs refuse without it); MCP: `confirm=true` parameter.
Agents are expected to ask the human before setting it.
- **Bulk permission delete ("all-delete") is not exposed at all** β its semantics
flipped between WORKS versions (masters-only vs *open to everyone*).
- Destructive semantics are spelled out in tool docs: drive `accessibleRange`
PATCH transitions wipe granted permissions; folder `enable` breaks inheritance;
`disable` drops folder grants.
## Setup
1. In the [Developer Console](https://developers.worksmobile.com/), create an app with
**service-account delegation**, note Client ID/Secret, create the service account, and
download the private key. Grant OAuth scopes (`file`, `user.read`).
2. Configure credentials:
```bash
pip install worksmobile-mcp # or: uv tool install worksmobile-mcp
mkdir -p ~/.config/worksmobile
cp .env.example ~/.config/worksmobile/.env # then fill in values
```
Config resolution: process env (`WORKS_*`) > `$WORKS_ENV_FILE` > `./.env` >
`~/.config/worksmobile/.env`.
## CLI
```bash
worksmobile doctor # what credentials/scopes actually work
worksmobile users --dept μ°κ΅¬ # members (joint appointments preserved)
worksmobile find νκΈΈλ # search by any substring
worksmobile audit --ignore shared-accounts.txt # account-hygiene findings
worksmobile drift roster.tsv --name-col name # reconcile against an external roster
worksmobile drives # list shared drives
worksmobile drive @2001000000xxxxxx # accessibleRange / permissionType
worksmobile ls --sd @2001000000xxxxxx # list files
worksmobile perms @2001000000xxxxxx --folder FID
worksmobile grant @2001000000xxxxxx --target pm@corp.com --type WRITE --folder FID --yes
worksmobile share FID --owner host@corp.com --to pm@corp.com --type WRITE --yes
worksmobile download FID --sd @2001000000xxxxxx -o report.hwp
worksmobile call GET /users/me/drive/files # raw API escape hatch
```
All read commands accept `--user someone@corp.com` to act as that member.
## MCP server
stdio (local agents β Claude Code, Codex, Cursor, Gemini CLI):
```bash
claude mcp add worksmobile -- worksmobile-mcp
```
```json
{ "mcpServers": { "worksmobile": { "command": "worksmobile-mcp" } } }
```
Streamable HTTP β **for a locally bound endpoint you front with your own auth**, not for
handing to a hosting provider (see the box at the top):
```bash
worksmobile-mcp --transport streamable-http --host 127.0.0.1 --port 8123
```
> β οΈ Whichever transport you pick, the process holds a tenant-wide admin credential β
> run it where you would run a domain-admin shell, and nowhere else.
### Tools
| Tool | Mutating | Description |
|---|---|---|
| `works_drives_list` / `works_drive_get` | | shared drives & settings |
| `works_files_list` | | files of a shared drive or a member's My Drive |
| `works_perms_list` | | drive/folder permissions |
| `works_perm_grant` / `works_perm_revoke` | β | grant / delete one permission |
| `works_folder_enable` / `works_folder_disable` | β | folder inheritance gate |
| `works_file_download` / `works_file_upload` | | storage-redirect download / 2-step upload |
| `works_folder_create` | | My Drive folder |
| `works_share_create` / `works_share_delete` | β | "shared with me" shortcuts (My Drive only) |
| `works_sharedfolders_list` | | a member's received shares |
| `works_search` | | drive search |
| `works_users_list` / `works_user_find` | | tenant members (needs `user.read`) |
| `works_directory_audit` | | account-hygiene findings + dormant-check report |
| `works_directory_drift` | | reconcile accounts against an external roster |
| `works_member_footprint` | | one member's full reach β the offboarding query |
| `works_api_call` | β | raw API escape hatch |
β = requires `confirm=true`.
### Directory audit: dormant checks
`works_directory_audit` returns `dormant_rules` alongside `findings`. A rule is dormant when
the field it reads is empty across the whole tenant β e.g. if nobody's `leaveOfAbsence` is
ever set, "on leave but not suspended" can never fire. **An empty findings list is not a
clean bill of health**, so the tool says which checks were powerless instead of implying
everything passed. `coverage` shows the fill rate per field.
This matters in practice: on the tenant this was built against, `employeeNumber` was 0/136
and `hiredDate` 1/136 β the HR fields simply were not populated, which is exactly the kind
of thing an audit tool must tell you rather than paper over.
### Reconciliation: absence from one roster is not evidence
`works_directory_drift` returns `only_works` β accounts with no entry in the roster you
passed. **That is a signal, not proof of departure.** Every roster omits some population:
an HR register may not carry visiting researchers at all; a chat directory cannot hold
people who were never issued corporate mail. Reading "not in this list" as "has left" is
how a current employee loses their account β which is exactly what happened once, and why
the tool now works this way.
Pass `corroborating_rosters` ({label: path}) with every other roster you have. Rows found
elsewhere come back with `seen_in` populated and should be dropped from suspicion. Each
roster is indexed by **email, phone and name**, because no single key is enough β in the
real case that motivated this, the member had no phone on their account and no corporate
mail, so only the name matched.
One trap worth knowing: normalising Korean mobile numbers by stripping `82` is wrong.
Many records carry the country code *and* the trunk zero (`+82 010-β¦`), so stripping alone
yields `00β¦` and matches nothing β 40% of one tenant's numbers. Use `directory.norm_phone`.
`blind_spots` covers the harder case: questions the API cannot answer *at all*. The one that
bites is **delegated administrators** β `isAdministrator` is true only for SUPER admins, so an
account that is a sub-admin in the console still reports `false`. Enumerating "who has admin
rights" from this API alone will silently miss them; do that in the admin console.
### Offboarding: collect the footprint *before* you suspend
`works_member_footprint` answers "what can this person reach" in one call. Two things it
encodes that are easy to get wrong:
- **Order.** Suspending the account first is the intuitive move and it destroys your ability
to investigate: delegation to a suspended account fails, so their My Drive contents and
received folders become unreachable. Collect first, suspend second.
- **Drive masters are invisible in `/permissions`.** They live on the shared-drive object's
`masters[]`. A master can grant permissions and change drive settings, so missing them in
a handover leaves the drive effectively ownerless β the tool reports them separately.
## API notes (hard-won)
- `accessibleRange` is 3-valued: `TENANT` / `DOMAIN` / `MEMBER`. New drives default to
DOMAIN+WRITE. **PATCH transitions are destructive** (MEMBERβDOMAIN deletes all grants;
βMEMBER fails while folder-level grants exist).
- Folder-level permissions accept **individual users only** (no org units); the target must
already be a drive member (undocumented, observed).
- Folder `enable` = masters-only until you grant; `disable` = grants dropped, inheritance back.
- share-create works on **My Drive folders only** β team-drive folders can't produce
"shared with me" shortcuts.
- A pure service-account token has **no My Drive** (403); hosting requires delegation to a
real account.
- Upload is 2-step (metadata POST β `uploadUrl` PUT); download is a 302 whose storage
location **also** requires the Bearer token.
- `GET /sharedrives` responds `{"sharedrives": [...]}` (docs imply a bare array).
## License
MIT
TDQS
Scored across 16 tools
Most tools have clearly distinct purposes, with only minor potential overlap between works_share_create/works_perm_grant and works_files_list/works_search. The descriptions generally resolve ambiguity adequately.
Tools follow a consistent noun_verb pattern (e.g., drive_get, files_list, perm_grant), with the exception of works_search, which lacks a resource prefix. This is a minor deviation from an otherwise predictable naming convention.
16 tools is slightly above the typical 3-15 range, but the scope covers file management, permissions, sharing, and a raw API escape hatch. Each tool serves a distinct purpose, making the count reasonable for the apparent domain.
The toolkit covers listing, downloading, uploading, sharing, and permission management well, but lacks file delete, rename, move, and drive lifecycle operations (create/update/delete). These gaps are notable for a file management server.