Metadata-Version: 2.5
Name: mcp-server-smartsheet-rm
Version: 2.1.1
Summary: Enterprise Model Context Protocol (MCP) server for Smartsheet Resource Management (10,000ft API) — time tracking, projects, allocations, scheduling, and capacity planning.
Project-URL: Homepage, https://github.com/christianclaudio/mcp-server-smartsheet-rm
Project-URL: Repository, https://github.com/christianclaudio/mcp-server-smartsheet-rm
Project-URL: Issues, https://github.com/christianclaudio/mcp-server-smartsheet-rm/issues
Project-URL: Documentation, https://github.com/christianclaudio/mcp-server-smartsheet-rm#readme
Project-URL: Changelog, https://github.com/christianclaudio/mcp-server-smartsheet-rm/releases
Author-email: Christian Claudio <christianclaudio@mail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: 10000ft,capacity-planning,mcp,model-context-protocol,resource-management,scheduling,smartsheet,time-tracking
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Requires-Dist: certifi>=2026.10.19
Requires-Dist: fastmcp>=4.1.3
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=2.3.0
Requires-Dist: pydantic-settings>=2.15.0
Requires-Dist: pydantic>=2.13.5
Provides-Extra: code-mode
Requires-Dist: fastmcp[code-mode]>=4.1.3; extra == 'code-mode'
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: fastmcp[code-mode]>=4.1.3; extra == 'dev'
Requires-Dist: mypy>=1.11.0; extra == 'dev'
Requires-Dist: opentelemetry-sdk<1.46,>=1.44.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Requires-Dist: respx>=0.21.0; extra == 'dev'
Requires-Dist: ruff<0.18,>=0.16; extra == 'dev'
Requires-Dist: twine>=5.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# 📋 mcp-server-smartsheet-rm

[![CI](https://github.com/christianclaudio/mcp-server-smartsheet-rm/actions/workflows/ci.yml/badge.svg)](https://github.com/christianclaudio/mcp-server-smartsheet-rm/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/mcp-server-smartsheet-rm)](https://pypi.org/project/mcp-server-smartsheet-rm/)
[![Python](https://img.shields.io/pypi/pyversions/mcp-server-smartsheet-rm)](https://pypi.org/project/mcp-server-smartsheet-rm/)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/christianclaudio/mcp-server-smartsheet-rm)
[![CodeRabbit Reviews](https://img.shields.io/coderabbit/prs/github/christianclaudio/mcp-server-smartsheet-rm?labelColor=171717&color=FF570A&label=CodeRabbit+Reviews)](https://coderabbit.ai)

Enterprise Model Context Protocol (MCP) server for **Resource Management by Smartsheet** (10,000ft API).

Enables AI coding agents, planners, and assistants (Claude, Cortex, Antigravity, VS Code) to work with the Smartsheet RM public REST API: time tracking & timesheet reconciliation, resource scheduling & allocations, capacity planning, project & phase management, leaves/holidays, expense tracking, and custom fields.

---

## 🏗️ System Architecture

```mermaid
graph TD
    Client["AI Agent (Claude / Cortex / Antigravity / Cursor)"] -->|"MCP Stdio / Streamable HTTP /mcp"| Server["Root Gateway create_server()"]
    Server --> Middle["Parent Middleware (ParentAudit / ReadOnly Gate)"]
    Middle --> SubTime["time namespace (time_* — 17 tools, incl. 1 bulk-gated)"]
    Middle --> SubProj["projects namespace (projects_* — 27 tools, incl. 1 bulk-gated)"]
    Middle --> SubAdmin["admin namespace (admin_* — 60 tools)"]
    SubTime --> Guards["Domain Guards"]
    SubProj --> Guards
    SubAdmin --> Guards
    Guards --> ClientPool["SmartsheetRMClient (httpx.AsyncClient Pool)"]
    ClientPool -->|"auth header + 429 Jitter Retry"| API["Smartsheet RM (10,000ft API)"]
```

`create_server` mounts the time, projects, and admin sub-servers with FastMCP 4 `namespace=`, so `tools/list` names are `time_*`, `projects_*`, and `admin_*`. Legacy `rm_*` names are not registered on the gateway.

---

## 🚀 FastMCP 4 Server Composition

* **Domain mounts** (`root.mount(..., namespace=...)`):
  * `namespace="time"` → `time_*` (17 tools, including the bulk-gated `time_bulk_delete_time_entries`).
  * `namespace="projects"` → `projects_*` (27 tools, including the bulk-gated `projects_bulk_delete_assignments`).
  * `namespace="admin"` → `admin_*` (60 tools).
* **Hierarchical middleware**:
  * **Parent**: `ParentAuditMiddleware` (timing, lifecycle logs, secret redaction) and `ReadOnlyGateMiddleware` (annotation-driven read-only gate, see below).
  * **Child**: `TimeDomainGuardMiddleware` (logged hours must be 0–24; bulk gate), `ProjectsDomainGuardMiddleware` (project names must be non-empty; bulk gate), `AdminDomainGuardMiddleware` (`per_page` must be ≤ 1000).

### Profiles

Pick a profile with `--profile` or `SMARTSHEET_RM_PROFILE` (default `full`). An unknown profile name fails at startup with `ValueError`. There are two kinds:

* **Domain-mount profiles** mount whole domains: `full`, `time`, `projects`, `admin`, and `readonly`.
* **Job profiles** mount every domain, then expose only an explicit list of tool names for one job. Prompts and resources stay available on every job profile and on `readonly`; the domain-mount profiles `time`, `projects` and `admin` carry only their own domain's prompts and resources. Each listed name is checked against the full catalog when the server builds, so a typo fails at startup.

| Profile | Job it serves | Tools | With `SMARTSHEET_RM_READONLY=1` |
| :--- | :--- | ---: | ---: |
| `full` | Complete catalog, nothing omitted. Bulk tools are listed and refused at call time unless `SMARTSHEET_RM_ALLOW_BULK_DESTRUCTIVE=1`. Tool Search and Code Mode attach only here. | **104** | 39 |
| `time` | Time tracking, approvals and timesheet recipes (time domain). | **17** | 4 |
| `projects` | Projects, phases, assignments, placeholders and subtasks (projects domain). | **27** | 10 |
| `admin` | Users, org reference data, clients, expenses, fields, reports and webhooks (admin domain). | **60** | 25 |
| `readonly` | Inspect without side effects: every tool annotated `readOnlyHint=True`. | **39** | 39 |
| `timesheets` | Team member or manager logs, corrects, submits, locks and approves weekly time against their assignments. | **24** | 12 |
| `staffing` | Resource manager checks availability and utilization and creates or adjusts assignments, placeholders and subtasks. | **27** | 18 |
| `org_setup` | Resource-management admin onboards people and maintains org reference data, custom-field definitions, user statuses and webhooks. | **35** | 13 |
| `portfolio` | PMO sets up and maintains projects, phases, clients, expenses and project metadata, and reads budget and report totals. | **33** | 18 |

Seven tools are in no job profile and are reachable only in `full` (or their domain-mount profile): `time_bulk_delete_time_entries`, `projects_bulk_delete_assignments`, `admin_delete_client`, `admin_delete_client_contact`, `admin_create_expense_category`, `admin_delete_expense_category`, and `admin_delete_tag`.

### Read-only behavior

* The MCP `readOnlyHint` annotation is the only thing that decides whether a tool is read-only. Every tool declares it explicitly: `true` on the 39 reads and `false` on the 65 writes. A tool with no annotation or no `readOnlyHint` counts as a write.
* `--profile readonly` or `SMARTSHEET_RM_READONLY=1` (on any profile) lists only the read-only tools, and `ReadOnlyGateMiddleware` refuses any call to a real tool that is not read-only, including a write the filter hid. A refusal comes back as a tool result with `isError: true`.
* A name that is not a tool on the server gets FastMCP's normal `Unknown tool` error, directly or through `call_tool`, so a typo is never reported as a blocked tool.
* With Tool Search on, the gate checks the tool that `call_tool` wraps. Reads through `call_tool` work and writes are refused. Without Tool Search, `call_tool` is not a tool on the server, so a call to it gets `Unknown tool: 'call_tool'`.
* If the gate cannot see the serving server, it refuses the call.

### Bulk gate

`time_bulk_delete_time_entries` and `projects_bulk_delete_assignments` are listed in `full`, `time` and `projects`. The domain guards refuse them at call time (`isError: true`) unless `SMARTSHEET_RM_ALLOW_BULK_DESTRUCTIVE=1`, and the handlers still require `confirm=True`. Read-only mode hides and refuses them.

### Tool Search and Code Mode

`tools/list` is flat by default. Discovery is opt-in and attaches **only on `full`**:

* `--enable-tool-search` / `SMARTSHEET_RM_ENABLE_TOOL_SEARCH=1` replaces `tools/list` with `search_tools` and `call_tool`. The backend is `regex` (default) or `bm25` (`--tool-search-backend` / `SMARTSHEET_RM_TOOL_SEARCH_BACKEND`).
* `--enable-code-mode` / `SMARTSHEET_RM_ENABLE_CODE_MODE=1` attaches FastMCP's experimental Code Mode (`search`, `get_schema`, `execute`). Code Mode needs the sandbox from the package's `code-mode` extra (`fastmcp[code-mode]`, which ships `pydantic-monty`): `pip install "mcp-server-smartsheet-rm[code-mode]"` or `uvx --from "mcp-server-smartsheet-rm[code-mode]" smartsheet-rm-mcp --enable-code-mode`. Without it, Code Mode is skipped with a warning and the flat catalog is served.
* Turning on both raises `ValueError`. Asking for either on another profile logs a warning and keeps the flat list.
* `search_tools`, `search` and `get_schema` only read the catalog and are annotated `readOnlyHint=True`. Under read-only, Code Mode `execute` is refused.

---

## ⚡ Tool Surface Overview

The server exposes tools covering projects, resources, timesheets, and capacity. Call the namespaced names below:
- **Default Registration**: 104 tools on `full` (the 2 bulk-destructive tools are listed and refused at call time unless `SMARTSHEET_RM_ALLOW_BULK_DESTRUCTIVE=1`).
- **Read-Only Mode**: 39 tools (`readOnlyHint=true`).
- **Destructive Gates**: 21 tools requiring explicit `confirm=True` (19 standard + 2 bulk).
- **Idempotent Operations**: 43 tools with `idempotentHint=true` (the 39 read-only tools, plus `time_update_time_approval_status`, `time_lock_timesheet`, `admin_set_custom_field_values`, and `admin_set_user_status`).

### Domain Overview
Agents call these `tools/list` names. Counts for time and projects include the bulk-gated tool.

1. **Time (`time_*`, 17 tools)**: `time_list_time_entries`, `time_get_time_entry`, `time_create_time_entry`, `time_create_user_time_entry`, `time_create_project_time_entry`, `time_update_time_entry`, `time_delete_time_entry`, `time_list_user_suggestions`, `time_update_time_approval_status`, `time_lock_timesheet`, `time_fill_weekly_timesheet`, `time_confirm_suggested_hours`, `time_reconcile_and_submit_week`, `time_list_approvals`, `time_create_approval`, `time_delete_approval`. Bulk-gated: `time_bulk_delete_time_entries`.
2. **Projects (`projects_*`, 27 tools)**: `projects_list_projects`, `projects_get_project`, `projects_create_project`, `projects_update_project`, `projects_delete_project`, `projects_list_project_users`, `projects_list_project_phases`, `projects_get_project_phase`, `projects_create_project_phase`, `projects_update_project_phase`, `projects_delete_project_phase`, `projects_list_assignments`, `projects_get_assignment`, `projects_create_assignment`, `projects_create_project_assignment`, `projects_create_user_assignment`, `projects_update_assignment`, `projects_delete_assignment`, `projects_clone_project_schedule`, `projects_list_status_options`, `projects_list_placeholder_resources`, `projects_create_placeholder_resource`, `projects_delete_placeholder_resource`, `projects_list_assignment_subtasks`, `projects_create_assignment_subtask`, `projects_delete_assignment_subtask`. Bulk-gated: `projects_bulk_delete_assignments`.
3. **Admin (`admin_*`, 60 tools)**:
   * Users, roles, disciplines, and capacity: `admin_list_users`, `admin_get_user`, `admin_create_user`, `admin_update_user`, `admin_delete_user`, `admin_list_user_bill_rates`, `admin_create_user_bill_rate`, `admin_get_user_availability`, `admin_get_user_utilization`, `admin_list_roles`, `admin_create_role`, `admin_update_role`, `admin_delete_role`, `admin_list_disciplines`, `admin_create_discipline`, `admin_update_discipline`, `admin_delete_discipline`.
   * Clients and contacts: `admin_list_clients`, `admin_get_client`, `admin_create_client`, `admin_update_client`, `admin_delete_client`, `admin_list_client_contacts`, `admin_create_client_contact`, `admin_delete_client_contact`.
   * Leaves and holidays: `admin_list_leave_types`, `admin_get_leave_type`, `admin_create_leave_type`, `admin_update_leave_type`, `admin_delete_leave_type`, `admin_list_holidays`, `admin_get_holiday`, `admin_create_holiday`, `admin_update_holiday`, `admin_delete_holiday`.
   * Expenses: `admin_list_expenses`, `admin_get_expense`, `admin_create_expense`, `admin_update_expense`, `admin_delete_expense`, `admin_list_expense_categories`, `admin_create_expense_category`, `admin_delete_expense_category`.
   * Tags and custom fields: `admin_list_tags`, `admin_create_tag`, `admin_delete_tag`, `admin_list_custom_fields`, `admin_get_custom_field`, `admin_create_custom_field`, `admin_update_custom_field`, `admin_delete_custom_field`, `admin_list_custom_field_values`, `admin_set_custom_field_values`.
   * Status, reports, and webhooks: `admin_get_user_statuses`, `admin_set_user_status`, `admin_get_report_rows`, `admin_get_report_totals`, `admin_list_webhooks`, `admin_create_webhook`, `admin_delete_webhook`.

### Undocumented routes

The tools below call routes that are in neither Smartsheet's [Resource Management API reference](https://developers.smartsheet.com/api/resource_management/openapi) ([OpenAPI JSON](https://developers.smartsheet.com/_spec/api/resource_management/openapi.json)) nor the archived [10,000ft API docs](https://github.com/10Kft/10kft-api). There are 46 such method and path pairs. They are kept, and not re-pointed, because some documented equivalents take different request shapes, so moving a tool would change what it sends. Smartsheet may change or remove an undocumented route without notice, and each of these tools says so in its description. Prefer a tool on a documented route when one fits: for example, `time_create_user_time_entry` or `time_create_project_time_entry` instead of `time_create_time_entry`. Each of these client calls carries `# drift-ignore: undocumented route`, and `scripts/check_openapi_drift.py` fails on any other call that is not in the published spec.

| Tool | Undocumented route | Documented equivalent |
| :--- | :--- | :--- |
| `time_create_time_entry` | `POST /time_entries` | `POST /users/{user_id}/time_entries` (`time_create_user_time_entry`) and `POST /projects/{project_id}/time_entries` (`time_create_project_time_entry`). |
| `time_update_time_entry` | `PUT /time_entries/{id}` | `PUT /users/{user_id}/time_entries/{id}`. |
| `time_delete_time_entry` | `DELETE /time_entries/{id}` | `DELETE /users/{user_id}/time_entries/{id}`. |
| `time_update_time_approval_status` | `PUT /users/{user_id}/time_entries/approval_status` | None found. |
| `time_lock_timesheet` | `POST /users/{user_id}/time_entries/lock` | None found. |
| `time_fill_weekly_timesheet` | `POST /time_entries` | `POST /users/{user_id}/time_entries`. |
| `time_confirm_suggested_hours` | `PUT /time_entries/{id}` | `PUT /users/{user_id}/time_entries/{id}`. |
| `time_reconcile_and_submit_week` | `PUT /users/{user_id}/time_entries/approval_status` | None found (called only with `auto_submit=True`). |
| `time_bulk_delete_time_entries` | `DELETE /time_entries/{id}` | `DELETE /users/{user_id}/time_entries/{id}`. |
| `projects_get_project_phase` | `GET /projects/{project_id}/phases/{phase_id}` | The reference documents only PUT and DELETE on that path; `GET /projects/{project_id}/phases` lists phases. |
| `admin_list_user_bill_rates` | `GET /users/{user_id}/bill_rates` | `GET /bill_rates` and `GET /projects/{project_id}/bill_rates`. |
| `admin_create_user_bill_rate` | `POST /users/{user_id}/bill_rates` | The reference documents bill rates only under `/bill_rates` and `/projects/{project_id}/bill_rates`, with no create. |
| `admin_get_user_availability` | `GET /users/{user_id}/availability` | `GET /users/{user_id}/availabilities`. |
| `admin_get_user_utilization` | `GET /users/{user_id}/utilization` | None found. |
| `admin_create_role` | `POST /roles` | The reference documents only GET for roles. |
| `admin_update_role` | `PUT /roles/{id}` | The reference documents only GET for roles. |
| `admin_delete_role` | `DELETE /roles/{id}` | The reference documents only GET for roles. |
| `admin_create_discipline` | `POST /disciplines` | The reference documents only GET for disciplines. |
| `admin_update_discipline` | `PUT /disciplines/{id}` | The reference documents only GET for disciplines. |
| `admin_delete_discipline` | `DELETE /disciplines/{id}` | The reference documents only GET for disciplines. |
| `admin_list_clients` | `GET /clients` | None found. |
| `admin_get_client` | `GET /clients/{id}` | None found. |
| `admin_create_client` | `POST /clients` | None found. |
| `admin_update_client` | `PUT /clients/{id}` | None found. |
| `admin_delete_client` | `DELETE /clients/{id}` | None found. |
| `admin_list_client_contacts` | `GET /clients/{id}/contacts` | None found. |
| `admin_create_client_contact` | `POST /clients/{id}/contacts` | None found. |
| `admin_delete_client_contact` | `DELETE /clients/{id}/contacts/{contact_id}` | None found. |
| `admin_create_leave_type` | `POST /leave_types` | The reference documents only GET for leave types. |
| `admin_update_leave_type` | `PUT /leave_types/{id}` | The reference documents only GET for leave types. |
| `admin_delete_leave_type` | `DELETE /leave_types/{id}` | The reference documents only GET for leave types. |
| `admin_get_holiday` | `GET /holidays/{id}` | The reference documents only `GET /holidays`. |
| `admin_create_holiday` | `POST /holidays` | The reference documents only `GET /holidays`. |
| `admin_update_holiday` | `PUT /holidays/{id}` | The reference documents only `GET /holidays`. |
| `admin_delete_holiday` | `DELETE /holidays/{id}` | The reference documents only `GET /holidays`. |
| `admin_list_expenses` | `GET /expenses`, `GET /projects/{project_id}/expenses`, `GET /users/{user_id}/expenses` | `GET /users/{user_id}/expense_items` and `GET /projects/{project_id}/expense_items`. |
| `admin_get_expense` | `GET /expenses/{id}` | `GET /users/{user_id}/expense_items/{expense_item_id}` and `GET /projects/{project_id}/expense_items/{expense_item_id}`. |
| `admin_create_expense` | `POST /expenses` | `POST /users/{user_id}/expense_items`. |
| `admin_update_expense` | `PUT /expenses/{id}` | `PUT /users/{user_id}/expense_items/{expense_item_id}`. |
| `admin_delete_expense` | `DELETE /expenses/{id}` | `DELETE /users/{user_id}/expense_items/{expense_item_id}`. |
| `admin_list_expense_categories` | `GET /expense_categories` | `GET /expense_item_categories`. |
| `admin_create_expense_category` | `POST /expense_categories` | The reference documents only `GET /expense_item_categories`. |
| `admin_delete_expense_category` | `DELETE /expense_categories/{id}` | The reference documents only `GET /expense_item_categories`. |
| `admin_list_tags` | `GET /tags` | `GET /projects/{project_id}/tags` and `GET /users/{user_id}/tags`. |
| `admin_create_tag` | `POST /tags` | `POST /projects/{project_id}/tags` and `POST /users/{user_id}/tags`. |
| `admin_delete_tag` | `DELETE /tags/{id}` | `DELETE /projects/{project_id}/tags/{tag_id}` and `DELETE /users/{user_id}/tags/{tag_id}`. |
| `admin_list_custom_field_values` | `GET /custom_field_values` | `GET /projects/{project_id}/custom_field_values` and `GET /users/{user_id}/custom_field_values`. |
| `admin_set_custom_field_values` | `PUT /custom_field_values` | `PUT /projects/{project_id}/custom_field_values/{custom_field_value_id}` and `PUT /users/{user_id}/custom_field_values/{custom_field_value_id}`. |

---

## 🚀 Quickstart & Installation

### 1. Run via `uvx`

Console scripts in `[project.scripts]` both call `smartsheet_rm_mcp.server:main`: `smartsheet-rm-mcp` (used below) and `mcp-server-smartsheet-rm`. Install examples stay unpinned. To freeze a release, pin the version from [Releases](https://github.com/christianclaudio/mcp-server-smartsheet-rm/releases).

```bash
uvx --from mcp-server-smartsheet-rm smartsheet-rm-mcp
```

After an upgrade, reload the MCP host so the live process start time is after the new binary mtime (stale process ≠ new package).

### 2. Installation

```bash
# Using uv (recommended)
uv pip install mcp-server-smartsheet-rm

# Or standard pip
pip install mcp-server-smartsheet-rm
```

### 3. Environment Variables

| Variable | Description | Default |
| :--- | :--- | :--- |
| `SMARTSHEET_RM_API_TOKEN` | Smartsheet RM (10,000ft) API Token (**Required**) | - |
| `SMARTSHEET_RM_BASE_URL` | Base API URL | `https://api.rm.smartsheet.com/api/v1` |
| `SMARTSHEET_RM_PROFILE` | Profile: `full`, `time`, `projects`, `admin`, `readonly`, `timesheets`, `staffing`, `org_setup`, `portfolio` (see [Profiles](#profiles)) | `full` |
| `SMARTSHEET_RM_READONLY` | Set to `1` to list only `readOnlyHint=true` tools and refuse every other call | `0` |
| `SMARTSHEET_RM_ALLOW_BULK_DESTRUCTIVE` | Set to `1` to let the two listed bulk delete tools execute | `0` |
| `SMARTSHEET_RM_ENABLE_TOOL_SEARCH` | Set to `1` (or `--enable-tool-search`) for Tool Search on `full` | `0` |
| `SMARTSHEET_RM_TOOL_SEARCH_BACKEND` | Tool Search backend: `regex` or `bm25` (or `--tool-search-backend`) | `regex` |
| `SMARTSHEET_RM_ENABLE_CODE_MODE` | Set to `1` (or `--enable-code-mode`) for experimental Code Mode on `full`; not with Tool Search | `0` |
| `SMARTSHEET_RM_MCP_AUTH_TOKEN` | Shared bearer token required on HTTP transports (stdio ignores it) | unset |
| `SMARTSHEET_RM_MCP_ALLOW_UNAUTHENTICATED_BIND` | Set to `1` to accept a tokenless HTTP bind to a non-localhost host | unset |
| `SMARTSHEET_RM_LOG_FORMAT` | Set to `json` for Datadog/CloudWatch structured logs | `text` |
| `SMARTSHEET_RM_ALLOWED_HOSTS` | Comma-separated hostnames allowed for base URL overrides. A non-blank value replaces the default. Loopback and private targets stay blocked. | `api.rm.smartsheet.com` |
| `SMARTSHEET_RM_GOV_SMARTSHEET_API_TOKEN` | Smartsheet API token for Smartsheet Gov only; sent as `Authorization: Bearer` and only to `app.smartsheetgov.com` (see [Smartsheet Gov](#smartsheet-gov)) | unset |

#### Smartsheet Gov

Smartsheet Gov is opt-in; the default base URL and allowlist do not change. Per Smartsheet's RM API [Introduction](https://developers.smartsheet.com/api/resource_management/introduction) and [Authentication](https://developers.smartsheet.com/api/resource_management/getting-started/authentication) pages, the Gov base URL is `https://app.smartsheetgov.com/2.0/rm/api/v1` and every request needs both the RM token in `auth` and a Smartsheet API token as `Authorization: Bearer`. To use it:

```bash
export SMARTSHEET_RM_BASE_URL='https://app.smartsheetgov.com/2.0/rm/api/v1'
export SMARTSHEET_RM_ALLOWED_HOSTS='app.smartsheetgov.com'
export SMARTSHEET_RM_API_TOKEN='your_rm_api_token'
export SMARTSHEET_RM_GOV_SMARTSHEET_API_TOKEN='your_smartsheet_api_token'
```

The Gov host goes through the same HTTPS, SSRF, allowlist and DNS-pinning checks as any other base URL. The client refuses to start against the Gov host without the Smartsheet token, and sends that token to no other host. On HTTP transports, a caller that sends its own `auth` or `x-smartsheet-rm-token` header must also send its own `x-smartsheet-gov-api-token` header; the environment Gov token is never paired with a caller's RM token.

---

## 💻 Client Configurations

### Google Antigravity (`~/.gemini/antigravity-cli/mcp_config.json`)

```json
{
  "mcpServers": {
    "smartsheet-rm": {
      "command": "uvx",
      "args": ["--from", "mcp-server-smartsheet-rm", "smartsheet-rm-mcp"],
      "env": {
        "SMARTSHEET_RM_API_TOKEN": "your-api-token"
      },
      "lazy": true
    }
  }
}
```

### Snowflake Cortex (`~/.snowflake/cortex/mcp.json`)

```json
{
  "servers": {
    "smartsheet-rm": {
      "command": "uvx",
      "args": ["--from", "mcp-server-smartsheet-rm", "smartsheet-rm-mcp"],
      "env": {
        "SMARTSHEET_RM_API_TOKEN": "your-api-token"
      }
    }
  }
}
```

### Claude Desktop (`claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "smartsheet-rm": {
      "command": "uvx",
      "args": ["--from", "mcp-server-smartsheet-rm", "smartsheet-rm-mcp"],
      "env": {
        "SMARTSHEET_RM_API_TOKEN": "your-api-token"
      }
    }
  }
}
```

### Streamable HTTP

Prefer Streamable HTTP. `--transport sse` is deprecated (MCP spec SEP-2577); `main()` logs a migration warning and does not advertise `/sse` as the client path.

```bash
uvx --from mcp-server-smartsheet-rm smartsheet-rm-mcp --transport streamable-http --host 127.0.0.1 --port 8000
```

Connect clients to `http://127.0.0.1:8000/mcp` (FastMCP's default Streamable HTTP path). `run(transport="streamable-http")` does not set a custom path. Binding to `0.0.0.0` or `::` requires an explicit `--allowed-host` (a wildcard `*` is rejected).

In the default (stateful) HTTP mode, a session idle for 30 minutes expires: its next request gets HTTP 404 and the client must start a new session. Change this with FastMCP's `session_idle_timeout` (on `http_app()`) or `FASTMCP_HTTP_SESSION_IDLE_TIMEOUT` (seconds, or `none` to never expire); see the [FastMCP 4.1.0 release](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.1.0) and [#5229](https://github.com/PrefectHQ/fastmcp/pull/5229).

**HTTP authentication.** Set `SMARTSHEET_RM_MCP_AUTH_TOKEN` to require `Authorization: Bearer <token>` on every HTTP request; a missing or wrong token gets `401`. The token is stripped, and a blank value counts as unset. It is attached when the server is built, so `smartsheet-rm-mcp`, `fastmcp run` and an ASGI host mounting `mcp.http_app()` all enforce it. stdio never uses it.

With no token, an HTTP bind to `127.0.0.1`, `::1` or `localhost` still starts, unauthenticated, and logs a warning. A tokenless bind to any other host exits with code 2. Set the token, bind to localhost, or set `SMARTSHEET_RM_MCP_ALLOW_UNAUTHENTICATED_BIND` to `1`, `true`, `yes` or `on` to accept an unauthenticated public bind (any other value refuses). To serve the image over HTTP, pass the tokens from your environment or an env file, not on the command line:

```bash
# export SMARTSHEET_RM_API_TOKEN and SMARTSHEET_RM_MCP_AUTH_TOKEN first, or use --env-file .env
docker run --rm -p 8000:8000 -e SMARTSHEET_RM_API_TOKEN -e SMARTSHEET_RM_MCP_AUTH_TOKEN \
  ghcr.io/christianclaudio/mcp-server-smartsheet-rm:latest \
  --transport streamable-http --host 0.0.0.0 --allowed-host localhost:8000
```

---

## 🛡️ Safety & Reliability

- **Secret Redaction**: API tokens, bearer headers, and sensitive keys are automatically scrubbed from errors and logs.
- **Destructive Gates**: Every deletion tool declares `confirm: bool = False` and makes no change unless the caller explicitly passes `confirm=True`. Without it the tool returns a normal result (`isError: false`) with `"status": "confirmation_required"` and a message to re-call with `confirm=true`; that is the designed two-step, not an error.
- **Input Validation**: Missing or invalid arguments (for example an update with no fields, or a malformed date) return a tool error (`isError: true`, `"type": "invalid_request"`) the model can correct, as the MCP spec describes for input validation errors.
- **Batch Results**: `time_fill_weekly_timesheet`, `time_confirm_suggested_hours`, `time_bulk_delete_time_entries` and `projects_bulk_delete_assignments` share one shape: `"status": "success"` or `"partial_success"`, a success count (`filled_count` / `confirmed_count` / `deleted_count`), `failed_count`, `results` and `errors`. Each success is `{id|date, status, result}`; each failure is `{id|date, status: "failed", error}` where `error` always has a `message` (API errors via `to_dict()`; other exceptions as `"type": "internal"` with a redacted message). The field names from earlier releases stay alongside: `time_fill_weekly_timesheet` also returns `days_filled`, `created_count` and `entries` (the created entries), and `time_confirm_suggested_hours` returns `confirmed_entries`. When every item fails, the call is a tool error (`isError: true`, `"type": "batch_failed"`).
- **Profiles**: Minimize token footprint by loading only the tools one job needs (`timesheets`, `staffing`, `org_setup`, `portfolio`) or one domain (`time`, `projects`, `admin`).
- **Read-Only Gate**: `readOnlyHint` decides; anything else is refused with `isError: true`.
- **Resilience**: Exponential backoff with randomized jitter on HTTP 429 rate limits.

---

## 🧪 Testing & Validation

```bash
# Run tests with 100% coverage requirement
pytest --cov=src/smartsheet_rm_mcp --cov-fail-under=100 -v

# Run Tool Contract verification
python scripts/check_tool_contract.py

# Run OpenAPI Drift check against Smartsheet's published spec (or --spec-file for a local copy)
python scripts/check_openapi_drift.py --spec-url https://developers.smartsheet.com/_spec/api/resource_management/openapi.json
```

<!-- mcp-name: io.github.christianclaudio/smartsheet-rm -->
