Skip to main content
Glama
z0nam

worksmobile-mcp

by z0nam
README.md
# 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

B3.4/5.0

Scored across 16 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness3/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues