clm-mcp
clm-mcp
An MCP server exposing the SELISE CLM Shipment
REST API (ClmShipmentWebService) to MCP clients — Claude Code, Claude Desktop, Cursor,
or any other MCP-compatible tool.
Every one of the 121 non-
Testoperations in the spec is reachable, by default, with zero configuration — 82 tools total:23 read-only tools covering shipments, incidents, site equipment, lean cards (working packages), material handovers, cockpit dashboards, and weather.
59 auto-generated write tools, one per
*Commandoperation (create/update/delete/ discard/...), individually named and annotated by risk.A full-coverage gateway (
clm_list_operations/clm_describe_operation/clm_invoke) that reaches literally every operation — including the ~44 less-common query operations without a hand-written tool — generically, with pre-flight schema validation.Set
CLM_ENABLE_WRITES=falseto opt out of the 59 write tools and run strictly read-only instead.
Proactive token refresh for a 7-minute access-token lifetime, correct handling of an API that returns HTTP 200 even for business failures, and response shaping so large DTOs don't flood the model's context.
Requirements
Python ≥ 3.13.3
A SELISE CLM account (username/password, or an existing refresh token)
Install
git clone <this-repo>
cd clm-mcp
uv syncAuthenticate
Pick one of these. If both are configured, environment variables win — the
on-disk store from clm-mcp login is only consulted when neither
CLM_REFRESH_TOKEN nor CLM_USERNAME+CLM_PASSWORD is set.
Option A — clm-mcp login (recommended)
A one-time interactive login. Your password is typed at a getpass prompt (never
echoed, never sent anywhere except the identity service, and never passed as a tool
argument — see Security). The resulting refresh token is validated and
saved to ~/.config/clm-mcp/credentials.json (mode 0600):
uv run clm-mcp login
# CLM username (email): you@example.com
# CLM password:
# Credentials saved to /home/you/.config/clm-mcp/credentials.jsonAlready have a refresh token (e.g. from your browser's network tab)? Skip the prompt:
uv run clm-mcp login --refresh-token <token>Option B — environment variables
Set one of these (e.g. in your MCP client's config, or a .env file — copy
.env.example to .env to start):
CLM_REFRESH_TOKEN=<token>
# — or —
CLM_USERNAME=you@example.com
CLM_PASSWORD=<password>If none of the above is configured, every tool call fails with a clear
ToolError telling you to run clm-mcp login.
Run
uv run clm-mcp # stdio — for MCP clients that spawn a subprocess
uv run clm-mcp --http --port 8000 # streamable HTTP — for a remote/containerized deployment--http also accepts --host (default 127.0.0.1) and --path (default /mcp).
Add to Claude Code
claude mcp add clm -- uv run --project /path/to/clm-mcp clm-mcpAdd to Claude Desktop
claude_desktop_config.json:
{
"mcpServers": {
"clm": {
"command": "uv",
"args": ["run", "--project", "/path/to/clm-mcp", "clm-mcp"]
}
}
}Credentials come from clm-mcp login automatically — no env block needed unless you
prefer CLM_REFRESH_TOKEN/CLM_USERNAME+CLM_PASSWORD inline.
See USAGE.md for a complete connection guide covering Claude Code, Claude
Desktop, Cursor, Windsurf, VS Code (Copilot Chat), Cline, Zed, Continue.dev, and generic
stdio/HTTP MCP clients, plus operating guidance for coding agents using this server.
Configuration reference
All variables use the CLM_ prefix (see .env.example for the full, commented list).
Variable | Default | Purpose |
| — | Refresh token (Option B) |
| — | Password grant (Option B) |
|
| Where |
|
| Business API base |
|
| Identity/token endpoint |
|
| Required |
|
| Register the 59 auto-generated write tools. Set to |
|
| Refresh this many seconds before actual expiry |
|
| Byte cap before a list response is truncated |
|
| Logging level (always written to stderr, never stdout) |
Tool reference
Identity & discovery
Tool | Description |
| Identity, site, and roles of the authenticated user — call this first. |
| Known integer values for CLM's undocumented status/type enums. |
Shipments
Tool | Description |
| Free-text shipment search, scoped to a site. |
| Filtered, searched, ordered, paged shipment list (cockpit list view). |
| Shipment counts by status bucket (open/approved/completed/cancelled). |
| Full internal shipment record by id (up to 142 fields). |
| Smaller, external-facing shipment summary — prefer this over |
| Comments posted on a shipment. |
| Audit/event log history for a shipment or related entity. |
| Day-wise timeline for a single shipment. |
Incidents
Tool | Description |
| Non-deleted incidents/findings raised against a shipment. |
| Full detail of a single incident/finding. |
Site equipment
Tool | Description |
| Bookable/non-bookable site equipment for the grid view. |
| Equipment timeline entries (bookings), filtered and paged. |
Lean cards (working packages)
Tool | Description |
| Lean cards for a site, filtered by status. |
| Lean card counts by status (overdue/active/pending/completed). |
Material handovers
Tool | Description |
| Handover summaries for a site, filtered by a required status string (the API rejects null/empty — see Known limitations) and date range. |
| Full handover detail, including event history and equipment usage. |
Cockpit dashboard
Tool | Description |
| Weekly shipment count grid for a site's unloading zones. |
| Daily weather records for a site within a date range. |
Full-coverage gateway
The curated tools above cover the common workflows. These three reach everything else in the spec (~100 more read/write operations) at the cost of the caller constructing its own request body:
Tool | Description |
| List operations by tag and/or search — e.g. |
| Get an operation's resolved JSON Schema request/response shape. |
| Execute any listed operation. Validates |
Test/* operations (see Security) are excluded from all three — they
never appear in clm_list_operations and clm_invoke refuses them by name.
Write tools (on by default — set CLM_ENABLE_WRITES=false to disable)
One tool per *Command operation (59 total), named clm_<tag>_<operation> — e.g.
clm_shipment_command_discard_shipment, clm_incident_command_create_incident. Each
is generated from that operation's resolved request schema and annotated by its verb:
Verb prefix | Annotation |
| not destructive |
| not destructive, idempotent |
| destructive |
anything else ( | destructive, non-idempotent (conservative fallback — annotations are hints, not a guarantee, so an unrecognized verb is treated as risky rather than safe) |
A deeply nested field (an array of objects, say) falls back to a permissive JSON
object rather than a fully-typed nested schema — the API itself still validates it.
Use clm_invoke instead when you want full JSON-Schema pre-validation of a nested body.
To see the exact generated names, ask a connected client to list its tools, or run:
npx @modelcontextprotocol/inspector uv run clm-mcpArchitecture
src/clm_mcp/
├── __main__.py CLI: clm-mcp / clm-mcp login / clm-mcp --http
├── config.py pydantic-settings, CLM_ env prefix
├── server.py build_server(): MCPServer + typed lifespan
├── enums.py domain enums (see Known limitations)
├── auth/ token lifecycle: models, on-disk store, TokenManager
├── api/ HTTP client (retry policy) + envelope unwrapping
├── spec/ vendored OpenAPI spec, operation registry, response shaping
├── services/ composition layer between tools and api/ — no business
│ logic in tool functions
└── tools/ MCP tool registration: meta, curated domains, gateway,
auto-generated write toolsAuth. The identity service issues a 7-minute access token. TokenManager
refreshes proactively (90s before expiry, configurable), coalesces concurrent
refreshes behind a single in-flight request, and forces a refresh on an unexpected
401. Refresh tokens observed on this API are not rotated, but the manager persists
whatever comes back regardless, in case that changes.
Envelopes. The business API returns HTTP 200 even when the requested operation
failed — success/failure lives in the JSON body (IsSuccess, ErrorMessages,
ExternalError, ValidationErrors.IsValid), not the status code. api/envelope.py
checks this on every response and raises a normal ToolError on failure, so the model
sees "shipment not found" the same way it sees any other tool outcome.
Response shaping. Some DTOs are large (Shipment has 142 properties, most
usually null for any given row). Every list-returning tool null-strips its result,
supports a fields parameter to project down to just what's needed, and enforces a
byte budget (CLM_MAX_RESPONSE_BYTES) — truncating rows (never a row's own fields)
with an explicit note rather than silently dropping data.
Development
uv sync --group dev
uv run pytest # full test suite
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy src/clm_mcp/ # strict type check
uv run python scripts/refresh_spec.py # check the vendored spec for drift
uv run python scripts/refresh_spec.py --write # ...and apply the updatenpx @modelcontextprotocol/inspector uv run clm-mcpopens the MCP Inspector against
this server for interactive testing (uv run mcp dev isn't usable here since the
entry point is a Typer CLI, not a module-level MCPServer instance the Inspector's
dev command can import directly).
Security
clm-mcp loginis the only place a password is collected, via a terminalgetpassprompt — never as a tool argument. Per the MCP specification, "servers MUST NOT use elicitation to request sensitive information," so this server never asks the client to collect one either. Only a refresh token is ever written to disk.Tokens are never logged. A structlog processor redacts
access_token,refresh_token,password, andAuthorizationfrom every log line, regardless of where in a nested structure they appear.Test/*operations are permanently excluded from this server (at the registry level — every tool,clm_list_operations, andclm_invokeall route through the same exclusion).Test/GetUserDataon the upstream staging API returns super-admin credentials in plaintext to any authenticated caller; this is an upstream vulnerability that should be reported and fixed independently of this client — excluding it here only stops this server from being a vector for it, it does not remediate the underlying exposure.Write tools are on by default (every
*Commandoperation gets a tool with zero setup) and individually annotated so an MCP client can prompt for confirmation before a destructive call — setCLM_ENABLE_WRITES=falsefor a connection that should never be able to mutate live data at all.
Known limitations
Several domain enums have no documented names.
ShipmentStatus(13 values),LeanCardStatus,Severity,ShipmentGroupingType,AccessType, andAvailableDateGetTypeare exposed as bare integers in the OpenAPI spec with no labels.enums.pyships them as explicitUNKNOWN_nplaceholders (not a guess — see that module's docstring) andclm_list_enumssurfaces the known int ranges. Tools accept either the placeholder name or the raw int. Replace the placeholders with real names (from the CLM front-end source or a domain expert) when available.The vendored spec's
servers[0].urlis wrong (plain HTTP, missing the gateway path prefix) — this server hardcodes the verified-correctCLM_API_BASE_URLdefault instead of reading it from the spec.No
securitySchemesin the spec — the Bearer scheme is inferred from testing against the live API, not documented. If the gateway ever requires an additional header,api/client.pyis the one place to add it.clm_list_material_handovers'sstatusis required, despite the spec marking the underlying fieldnullable: true— the API rejects both a missing and an empty/nullStatuswith'Status' must not be empty.statusis matched as a literal value, not a wildcard:Allis accepted without a validation error but matches zero handovers (confirmed live:TotalCount=0against a site with 35 real handovers).InProgressis confirmed live to filter correctly (returned all 35 matching handovers on that site).Open,Completed,Pending,Draft, andClosedare accepted without a validation error but their filtering semantics were not confirmed against real data.Two upstream endpoints return a 500 instead of a clean business error, reproduced identically via raw
curl(not an artifact of this client):LeanCardQuery/GetWorkingPackagesandGetWorkingPackagesCount(Object reference not set to an instance of an object), andTimelineQuery/GetShipmentTimelineByIdfor a nonexistent shipment id (Exception has been thrown by the target of an invocation).clm_list_working_packages,clm_count_working_packages, andclm_get_shipment_timelinesurface these faithfully asToolErrors — there is nothing to fix client-side.
Troubleshooting
Symptom | Likely cause / fix |
| Run |
| Check the credentials themselves against the CLM login page. |
| It expired or was revoked — run |
A tool call is missing/behaves oddly after an upstream API change | Run |
Response was truncated | The |
Nothing happens over stdio / a client can't parse output | Something wrote to stdout directly — all logging in this project goes to stderr by design ( |
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sabadia/clm-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server