Timill ships a Model Context Protocol server bundled inside the platform itself, so AI agents (Claude, and any MCP-capable host) can read and write your Timill data the same way the REST API does — e.g. “Create a goal and a few tasks for John, blocked by the Q2 launch.”
It is served over Streamable HTTP at the path /mcp on your instance, and exposes a small set of generic tools (no hardcoded item types — they adapt to your group’s schema). Every call is authenticated per-user and enforced by the same RBAC that governs the rest of the app: an agent can only ever see and do what the authenticating user can.
This page is written for agents and the people configuring them. If you are looking for the HTTP/JSON surface, see the REST API.
What is Timill? (Read this before assuming a domain)#
Timill is a configurable work platform, not a fixed CRM. Do not assume it tracks leads, deals, or contacts — that is only true if this particular instance was set up that way.
The model is deliberately generic:
- A group is a workspace. Each group defines its own item types, custom fields, statuses, and lists — its own schema.
- An item is any record of one of those types: a sales lead, a purchase order, a restaurant table reservation, a support ticket, a manufacturing work order, a recipe, a maintenance job, a job applicant — whatever the group was configured for.
So the same tools (query_items, create_items, …) operate on a CRM in one group, an ERP/inventory system in another, and a restaurant or clinic booking system in a third. There are no built-in concepts of “lead”, “deal”, “product”, or “customer” — those, if they exist at all, are just item types and fields a human defined for that group.
Because of this, never guess the schema — discover it. A request like “find my open leads” may map to a Lead type in one instance, an Opportunity type in another, or nothing at all. Always:
list_groups→ see which workspaces exist and pick the relevant one.get_group_config(groupId)→ learn that group’s actual item types, field ids, statuses, and members.- Then build your
query_items/create_itemscalls against those real ids.
If nothing in the group’s schema matches what the user asked for, say so — don’t invent a “leads” concept that this instance doesn’t have.
Which server URL do I use? (Read this first)#
There is no single global MCP endpoint. Each Timill instance runs its own MCP server at its own base URL. You must point your agent at the URL of the instance that holds your data.
| Your deployment | Your MCP URL |
|---|---|
| Timill Cloud (shared SaaS) | https://app.timill.com/mcp — the default public server |
| Dedicated cloud instance | https://<your-subdomain>.timill.com/mcp (your dedicated host) |
| Self-hosted / private instance | https://<your-platform-host>/mcp |
https://app.timill.com/mcp is only the default public server for the shared Timill Cloud. It can only reach data that lives on app.timill.com. If you are on a private (self-hosted) or dedicated cloud instance, the public server cannot see your groups, items, or users at all — and its OAuth login is a different account namespace.
To get full support against your own data, you must connect to your own instance’s /mcp URL. The MCP server, its OAuth authorization server, and the discovery endpoints are all hosted on that same instance, so everything (login, consent, tokens, tools) is self-contained per instance.
Operators: the MCP server is gated by an instance-level Enable MCP toggle (Instance Admin → Settings, or the
TIMILL_ENABLE_MCPenv var; default on). When disabled,/mcpand all OAuth endpoints return404— the feature is simply not advertised. No restart is needed to flip it.
Connecting#
Most MCP hosts take a server URL and handle the OAuth login for you. Minimal client config:
{
"mcpServers": {
"timill": {
"url": "https://your-instance-host/mcp"
}
}
}
Replace the host with your instance (see the table above). On first connect the host opens a browser, you log in to Timill and consent, and the agent is issued a token automatically — no manual token paste required.
Authentication#
The /mcp endpoint accepts a bearer token in the standard header:
Authorization: Bearer <token>
There are two supported ways to obtain that token:
1. OAuth 2.1 + PKCE (recommended — for MCP hosts like Claude)#
Timill acts as a full OAuth 2.1 Authorization Server for its own MCP server. MCP hosts discover and complete the flow automatically — you only log in and click Allow. Under the hood:
- The user authenticates to Timill however they normally do (password, Google, Microsoft, or OIDC) and explicitly consents.
- Timill mints its own opaque access + refresh tokens, scoped to the single
mcpscope. (The underlying login method is irrelevant to the token.) - Access tokens last 1 hour; refresh tokens last 90 days. The host refreshes silently.
On a missing/invalid token the server returns 401 with a WWW-Authenticate challenge pointing at the discovery document, so compliant hosts can start the flow themselves.
Discovery & OAuth endpoints (all on your instance’s base URL, public, CORS-enabled):
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-protected-resource | RFC 9728 — names the authorization server protecting /mcp |
GET /.well-known/oauth-authorization-server | RFC 8414 — authorization-server metadata |
GET /.well-known/openid-configuration | Alias of the above (some hosts probe this) |
POST /oauth/register | RFC 7591 — dynamic client registration |
GET /oauth/authorize | Authorization endpoint (login + consent) |
POST /oauth/token | Token endpoint (authorization_code, refresh_token) |
GET /oauth/jwks | Empty key set (tokens are opaque, validated by lookup) |
The protected-resource identifier is exactly
<base-url>/mcp— it must match the URL the client connects to.
2. Personal API token (manual / scripted clients)#
The same tim_… bearer tokens used by the REST API also work on /mcp. Generate one in the app under User Settings → API Keys, then supply it via your client’s auth/header config:
Authorization: Bearer tim_xxxxxxxxxxxxxxxxxxxxxxxx
This path is handy for scripts and non-interactive agents that can’t run a browser OAuth flow.
Permissions & RBAC#
Whichever token is used, the agent acts as that user. There is no elevation — instance-admin context is never applied to MCP calls. RBAC is enforced inside the platform’s core logic on every call:
- Group access requires membership (deleted/inaccessible groups are hidden).
query_itemsapplies row-level RBAC filters — agents only see items their role allows.- Every create/update/delete/comment re-checks the relevant permission and field-level rules.
Per-tool permission requirements:
| Permission | Tools |
|---|---|
| (none — self-scoped) | whoami |
GROUP_READ | list_groups, get_group_config, get_list, resolve_field_options |
ITEM_READ | query_items, get_item, list_comments |
ITEM_CREATE | create_items |
ITEM_UPDATE | update_items |
ITEM_DELETE | delete_item |
ITEM_COMMENT | add_comment |
GROUP_ADMINISTRATION | add_list_options |
Tool catalog#
Thirteen tools, in three groups. Inputs and outputs are schema-typed — the host is forced to send conforming arguments and receives structured results.
Discovery & schema#
| Tool | Purpose | Key inputs |
|---|---|---|
whoami | Identify the authenticated caller — returns {userId, name}. Use to resolve “me”/“my” (e.g. assign to self, or filter query_items by your own id). | (none) |
list_groups | List the groups the user can access — [{id, name}]. Start here to discover which group to work in. | (none) |
get_group_config | The “read the config” call. Lean snapshot of a group’s schema: per item type its custom fields ({id, name, type, listId?, options?, optionsTruncated?, ref?}) and statuses ({id, name}), plus a light member roster ({userId, name}) for resolving names like “John”. List-backed dropdown options are inlined per field (capped at 50). Run before querying or creating items. | groupId |
get_list | Fetch the complete option set [{id, name}] of one custom list. Only needed when a field reported optionsTruncated: true. | groupId, listId |
resolve_field_options | The smart linking tool. Returns candidate [{id, name}] for a reference or dropdown field, search-filtered and capped. Handles ItemREF/UserREF/dropdown, cross-group scope, and dynamic queries server-side. Use it to find the id to set on a reference field (e.g. link a task to “the launch item”). | groupId, itemTypeId, fieldId, itemId?, search?, limit? (default 25, max 100) |
add_list_options | Append new options to an existing custom list (e.g. add a missing dropdown value before setting it). Returns the created options with server-generated ids. (Creating/deleting whole lists is out of scope.) | groupId, listId, options: [{name, color?}] |
Items#
| Tool | Purpose | Key inputs |
|---|---|---|
query_items | The workhorse read. Two modes — pick one per call: (1) Structured (query DSL + type/status/assignee filters, sort/order, limit/offset) for precise, sortable, paginated results; (2) Search (search) for fuzzy, relevance-ranked keyword lookup over title/description (when search is set the structured filters and sort/offset are ignored). Default output is a compact row {id, idInGroup, title, type, status, assignee} plus {total, limit, offset, hasMore}. Pass fields (custom/built-in ids, or ["all"]) to attach a raw {fieldId: value} map per row and skip per-row get_item. | groupId, plus query?, type?, status?, assignee?, search?, sort?, order?, limit? (default 50, max 100), offset?, fields? |
get_item | One item in full detail: all built-in fields plus every custom field with datatype metadata. Prefer query_items with fields to read many items at once. | groupId, itemId |
create_items | Batch create (e.g. a goal + several tasks in one call). title and type required per item; items are created independently — one failure doesn’t abort the rest. Returns a per-item result with ok/error and per-field violations. | groupId, items: [{title, type, description?, status?, assignee?, dueDate?, customFields?}] |
update_items | Batch partial update. Each patch is keyed by itemId; only included fields change, customFields are merged. Applied independently; check each result. | groupId, items: [{itemId, title?, description?, status?, assignee?, dueDate?, customFields?}] |
delete_item | Soft-delete — moves the item to the group’s trash (hidden from default queries, recoverable in the app). Destructive; confirm the itemId first. | groupId, itemId |
Comments#
| Tool | Purpose | Key inputs |
|---|---|---|
list_comments | An item’s comment thread ({id, author, content, created}), newest first, paginated. | groupId, itemId, limit? (default 50, max 100), offset? |
add_comment | Post a comment on an item. Auto-subscribes the author and any @mentioned members and notifies them. | groupId, itemId, body |
The query_items query DSL#
Used in the query argument (structured mode). Predicates combine with AND / OR / NOT and ( ) grouping; adjacent terms are implicitly AND-ed.
| Pattern | Meaning |
|---|---|
field:value | Equality on selection fields; case-insensitive contains on free-text (title/description) |
field:~text / field:^text / field:text$ | contains / starts-with / ends-with |
field:/regex/ / field:*.pdf | regex / wildcard |
field:>5 >=5 <5 <=5 | comparisons |
field:1..10 / dueDate:2026-01-01..2026-03-01 | inclusive range |
status:(open pending closed) | IN list (any of) |
field:$empty / field:$filled | presence check |
created:$today $yesterday $thisweek $thismonth $thisyear | relative dates |
dueDate:$today-7days | trailing window |
Built-in field keys: title, description, status, assignee, author, type, dueDate, completedDate, created, updated, idInGroup (sub-fields assignee.email, author.email). Custom fields are referenced by their field id (from get_group_config); dropdown/reference values by option/target id — e.g. <fieldId>:<optionId>.
Sortable fields (structured mode only): created, updated, title, status, assignee, dueDate, completedDate, idInGroup. Custom fields cannot be sorted.
status:open AND assignee:<userId> AND dueDate:<2026-05-01
(status:open OR status:pending) AND dueDate:$filled
NOT status:closed AND created:$thismonth
<dropdownFieldId>:<optionId> AND <dateFieldId>:>=2026-01-01
Custom-field value encoding#
When writing customFields (in create_items / update_items), key the map by custom-field id and encode the value by field type:
| Field type | Value |
|---|---|
| Dropdown / list | the option id |
| ItemREF | the target item id; ItemsREF → array of item ids |
| UserREF | the user id; UsersREF → array of user ids |
| Date | YYYY-MM-DD string |
| Number / String / Boolean | the scalar |
Resolve the needed ids with get_group_config (dropdown options are inlined) or resolve_field_options. Built-in fields (title, description, status, assignee, dueDate) are set directly, not via customFields.
Ambiguous wall-clock dates resolve against the token user’s timezone (default UTC).
Batch result shape & error handling#
create_items / update_items never fail the whole batch on one bad item — they return a per-item result so the agent can self-correct:
{ "results": [
{ "index": 0, "ok": true, "id": "0199…", "idInGroup": 42 },
{ "index": 1, "ok": false, "error": "unknown item type \"tsak\"",
"fields": [ { "fieldId": "…", "kind": "required-field-missing", "message": "…" } ] }
] }
Check each ok/error; on validation failure the fields array names exactly which field failed and why.
Typical agent workflow#
“Create a goal and some tasks for John”
list_groups→ pick the group (or take it from context).get_group_config(groupId)→ get John’suserIdfrom the roster, the goal/task type ids, and required field ids (dropdown options are inlined).create_items(groupId, [...])→ batch the goal + tasks in one call.
That’s ~2–3 calls. Linking a task to another item adds one resolve_field_options(..., search: "launch") to fetch the target id. Reading many items’ fields at once? Use query_items with fields instead of looping get_item.
Notes & limits#
- Rate limiting:
/mcpshares the platform’s API rate limiter. - Search cap: the underlying relevance search caps its result set;
limitfurther trims the page. - Reference resolution during creation: when resolving reference candidates for an item that doesn’t exist yet, dynamic queries that depend on the item’s other (not-yet-set) field values may be incomplete — rely on the
searchfilter inresolve_field_options. - Out of scope (today): creating/deleting whole lists, and item-type / group / role mutation. Web search is provided by the agent host, not Timill.