# Connect to the TimeDoctor MCP server

The TimeDoctor MCP server lets AI clients (Claude, ChatGPT, Gemini, Cursor,
VS Code, …) **read** your TimeDoctor data through a small set of safe, read-only
tools — directory data (users, projects, tasks, groups/tags, work schedules and
leave) plus time-tracking analytics (tracked/idle time, productivity, web & app
usage, worklog timeline and meetings). This guide shows how to connect from each
client.

> Placeholders to fill in:
> - `MCP_URL` — your server endpoint. In production this is served under the
>   platform base path, typically `https://<host>/api/2.0/mcp` (local dev:
>   `http://localhost:8092/api/mcp`). Use whatever your admin gives you.
> - `TD_TOKEN` — a TimeDoctor access token, for clients that send a static
>   token. OAuth clients sign in instead and do not paste a token.
> - `COMPANY_ID` — optional company id from `td_list_companies`. It is not
>   required to connect. One OAuth connection can use every company that tool
>   returns.
>
> The published docs/llms.txt links below adapt automatically to the base path
> the server is exposed under (via `X-Forwarded-Prefix`),
> so they stay correct under `/api/2.0/mcp` or any other prefix.

## Connection model (read this first)

- **Transport:** MCP **Streamable HTTP** (the current remote transport).
- **Auth:** OAuth sign-in, or `Authorization: Bearer <TD_TOKEN>` on static-token
  clients.
- **Company:** after connecting, call `td_list_companies`, then pass `company`
  (the id, never the name) on each other tool call. One company in the list is
  selected automatically. Several companies require `company` on every call.
- **Optional request default:** `x-company-id: <COMPANY_ID>` or
  `?company=<COMPANY_ID>`. On OAuth, that value is used only when the call omits
  `company` (the argument wins). A static TimeDoctor access token selects the
  company with the header or query; change it to reach another company you
  belong to. API keys ignore it and stay on the company they were created for.
- **Where the connection comes from:** cloud clients (Claude.ai, ChatGPT) reach
  the server **from the vendor's cloud**, so `MCP_URL` must be reachable over the
  public internet. Desktop/CLI clients connect from your machine.
- **stdio-only clients** (older configs) use the **`mcp-remote`** npm bridge
  (needs Node.js installed).

Header samples below may include `x-company-id`. For a multi-company OAuth
account, omit that header, connect, then follow **Verify it works**.

Everything is **read-only**. Each call is tenant-scoped to the company selected
for that call.

### Published docs (served by the server)

The server also publishes this guide and an `llms.txt` map at runtime (public, no
auth). `<base>` is the service base path (e.g. `/api/2.0/mcp` in production, `/api`
in local dev):

- `GET <host><base>/guide` — docs index (HTML)
- `GET <host><base>/guide/setup` · `<base>/guide/agent-guide` · `<base>/guide/composable-tools` · `<base>/guide/tool-reference` — markdown
- `GET <host><base>/llms.txt` — agent/crawler map (point AI tools here)
- `GET <host><base>/llms-full.txt` — single-fetch concatenation of setup + agent-guide + tool-reference
- `GET <host><base>/.well-known/llms.txt` — same body as `/llms.txt`
- `GET <host><base>/.well-known/llms-full.txt` — same body as `/llms-full.txt`

OAuth Protected Resource Metadata (RFC 9728) is published at:

- `GET /.well-known/oauth-protected-resource/api/2.0/mcp/mcp`
- `GET /api/2.0/auth/.well-known/oauth-protected-resource`

OAuth Authorization Server Metadata (RFC 8414) is also answered at the API host
root, mirrored from the authorization server. Clients that probe the host root
find it there; the Gemini Enterprise table below lists the authorization
server's own discovery URL.

- `GET /.well-known/oauth-authorization-server`

---

## Claude Desktop (config file)

Open **Settings → Developer → Edit Config** (or edit the file directly):
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "timedoctor": {
      "type": "http",
      "url": "MCP_URL",
      "headers": {
        "Authorization": "Bearer TD_TOKEN",
        "x-company-id": "COMPANY_ID"
      }
    }
  }
}
```

Static token only. Omit this header on OAuth.

Fully **quit and reopen** Claude after saving. If your Claude build doesn't accept
custom headers in the config, use the `mcp-remote` bridge below.

## Claude.ai (web) / Claude Desktop — Custom Connector (no config file)

**Settings → Connectors → Add custom connector** → name it, paste `MCP_URL`,
complete auth, then enable the tools you want. (Requires the server to be on the
public internet.)

## Claude Code (CLI)

```bash
claude mcp add --transport http timedoctor MCP_URL \
  --header "Authorization: Bearer TD_TOKEN" \
  --header "x-company-id: COMPANY_ID"
```

Static token only. Omit this header on OAuth.

## ChatGPT (Developer Mode)

Plus/Pro/Business/Enterprise. **Settings → Connectors → Advanced → enable
Developer mode** → **Add custom connector** → name + `MCP_URL` → choose auth →
save. After server changes, open the connector and click **Refresh** to re-pull
the tools. ChatGPT honors the read-only hint, so these tools won't prompt for
write confirmation. If your ChatGPT plan only offers OAuth-based connectors and
you have a static token, use the `mcp-remote` bridge instead.

## Google Gemini CLI

Edit `~/.gemini/settings.json` (or project `.gemini/settings.json`):

```json
{
  "mcpServers": {
    "timedoctor": {
      "httpUrl": "MCP_URL",
      "headers": {
        "Authorization": "Bearer TD_TOKEN",
        "x-company-id": "COMPANY_ID"
      },
      "timeout": 30000
    }
  }
}
```

Static token only. Omit this header on OAuth.

(`httpUrl` = Streamable HTTP.) Verify in a session with `/mcp`.

## Gemini Enterprise

Gemini CLI (the section above) runs on your computer. Gemini Enterprise is the
Google Cloud admin console. It does not support dynamic client registration, so
the server URL alone is not enough. Time Doctor registers one shared public
client. Paste the values below into the custom MCP server form. Turn **Enable
PKCE Support** on. Leave **Client Secret** empty. Do not create or share a
client secret.

**Client ID:** client_01M470P5NE75FZEDKG5BTR0X9Z

Confirm endpoints with the RFC 8414 discovery URL in the table. Do not use
`/.well-known/openid-configuration`.

The redirect URI registered on the client is
`https://vertexaisearch.cloud.google.com/oauth-redirect`. Google sends it. You
do not type it into the form.

### Production

These values match the Client ID above.

| Field | Value |
| --- | --- |
| MCP Server URL | `https://api2.timedoctor.com/api/2.0/mcp/mcp` |
| Authorization URL | `https://oauth.timedoctor.com/oauth2/authorize` |
| Authorization URL Parameters | `&resource=https://api2.timedoctor.com/api/2.0/mcp/mcp` |
| Token URL | `https://oauth.timedoctor.com/oauth2/token` |
| Client Secret | Leave blank |
| Enable PKCE Support | On (`S256`) |
| Use HTTP Basic Authentication | Off (the form turns it on by default) |
| Scopes | `openid profile email offline_access` |
| Discovery (not a form field) | `https://oauth.timedoctor.com/.well-known/oauth-authorization-server` |

Allow these egress domains in the Google Cloud organization policy:
`api2.timedoctor.com` and `oauth.timedoctor.com`.

1. Click Verify Auth, sign in, then Create (location Multi-region).
2. Open the data store → Actions → Reload custom actions → select all → Enable actions.
3. Apps → your app → Connected data stores → add the Time Doctor data store.
4. Each user: in the Gemini chat, Connectors → Time Doctor → Authorize, and sign in with their own Time Doctor account.

Private cloud (your own Time Doctor host): not supported yet. Contact support; do not use these values.

## Cursor

Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):

```json
{
  "mcpServers": {
    "timedoctor": {
      "url": "MCP_URL",
      "headers": {
        "Authorization": "Bearer ${env:TD_TOKEN}",
        "x-company-id": "COMPANY_ID"
      }
    }
  }
}
```

Static token only. Omit this header on OAuth.

Use `${env:TD_TOKEN}` so the token isn't committed.

## VS Code (Copilot agent)

Edit `.vscode/mcp.json` (workspace) or your user MCP config. Note the top-level
key is `servers` and `type: "http"`:

```json
{
  "inputs": [
    { "id": "tdToken", "type": "promptString", "description": "TimeDoctor token", "password": true }
  ],
  "servers": {
    "timedoctor": {
      "type": "http",
      "url": "MCP_URL",
      "headers": {
        "Authorization": "Bearer ${input:tdToken}",
        "x-company-id": "COMPANY_ID"
      }
    }
  }
}
```

Static token only. Omit this header on OAuth.

## `mcp-remote` fallback (any stdio-only client, or for header injection)

Requires Node.js (`npx`):

```json
{
  "mcpServers": {
    "timedoctor": {
      "command": "npx",
      "args": [
        "mcp-remote", "MCP_URL",
        "--header", "Authorization: Bearer ${TD_TOKEN}",
        "--header", "x-company-id: ${COMPANY_ID}",
        "--header", "x-client-name: claude"
      ],
      "env": { "TD_TOKEN": "TD_TOKEN", "COMPANY_ID": "COMPANY_ID" }
    }
  }
}
```

Static token only. Omit this header on OAuth.

> Set `x-client-name` to the host product (`claude`, `cursor`, `chatgpt`, …) when using
> `mcp-remote` — otherwise usage analytics may only see a generic Node User-Agent.
> Direct HTTP clients that send MCP `initialize.clientInfo` are labeled
> automatically.
>
> Optional analytics headers: `x-mcp-conversation-id` (stable
> per user question), `x-mcp-turn-id`, `x-mcp-root-request-id`, `x-mcp-parent-call-id`,
> `x-mcp-is-retry`. Agents can also pass the same fields as tool args. If omitted,
> the server writes a **1-call baseline** only (`conversationId=requestId`,
> `turnId=conversationId`) — that does **not** merge multi-call questions; send a
> stable `conversationId` (`telemetry.conversationId`, header, or legacy tool arg) for true question grouping.
>
> Optional analytics headers: `x-mcp-query` (hashed only — never stored raw)
> and `x-mcp-question-category` (allowlisted override; usually omit so the server
> derives the category from the tool).

---

## Verify it works

After connecting, ask the client to **list tools** — you should see the `td_*`
tools (e.g. `td_list_companies`, `td_whoami`, `td_list_users`, `td_list_projects`,
`td_get_time_summary`). Then:

1. Call `td_list_companies` and read the ids.
2. One company: later calls may omit `company`.
3. Several companies: pass the chosen id as `company` on every other call.
   If two names match or overlap, list the name and id and ask — do not guess.

Then try: *"Who am I connected as?"* (`td_whoami`) or *"List the first few
projects."* Include `company` when step 3 applies.

### Server instructions

Every successful `initialize` includes slim Tier A text in `result.instructions`
(interpretation guardrails, timeout, timezone, tool chooser). To read them
without a host UI, POST `initialize` to `MCP_URL` and inspect that field. Some
clients only inject the text into the model after the first Time Doctor tool
schema is loaded — that is expected. `llms.txt` does not replace this field.
Connections that go only through a local MCP bridge/proxy are a separate gap.

## Troubleshooting

| Symptom | Likely cause / fix |
|---|---|
| No server / all servers vanish | Invalid JSON (a stray comma). Validate the file. |
| `command not found` / `npx` errors | Install Node.js; pre-test with `npx -y mcp-remote MCP_URL`. |
| 401 / `unauthorized` | Missing or expired token → sign in again or regenerate `TD_TOKEN`. A rejected static-token `x-company-id` can also 401. |
| Call says select a company | Multi-company OAuth and the call omitted `company`. Call `td_list_companies`, then pass the id. |
| Gemini Enterprise `invalid_scope` or the wrong token URL | Use `openid profile email offline_access`, leave Client Secret blank, enable PKCE, turn HTTP Basic Authentication off, and take the URLs from the RFC 8414 discovery row. Do not use `openid-configuration`. |
| Gemini Enterprise works, then stops after about an hour (401 "token expired") | Add `offline_access` to the connector Scopes, then Re-authenticate the connector and Authorize again. |
| Gemini Enterprise: "This account does not have permission to use this connection" (403), or Gemini says there is no Time Doctor integration | The signed-in Time Doctor account has no company where AI Connections is allowed for its role. Ask the Time Doctor admin. |
| Gemini Enterprise: "not allowed to perform this operation" when you click Authorize | Reload the page and click Authorize again. Right after a connector is created or re-authenticated, this can take a few minutes. Do not disconnect and reconnect the connector. |
| Gemini Enterprise answers with another account's data after you sign in to a different Time Doctor account | Start a new chat after you switch accounts. When a tool call fails, Gemini can reuse answers from earlier in the same chat. |
| `forbidden` | MCP isn't enabled for your workspace yet, or your role lacks access. Contact your admin. A wrong company id also returns forbidden: call `td_list_companies` and retry with a listed id. |
| `Forbidden origin` / HTTP 403 | Browser `Origin` is not on the server's allowed-origins list. Desktop/CLI clients that send no Origin are unaffected. Ask an admin to add the client origin. |
| Connects but no tools | Wrong transport field for that client (`httpUrl` vs `url` vs `type:"http"`), or you need to Refresh (ChatGPT) / restart the client. |
| Tool “didn't load” on first try, works on retry | Usually **client-side** discovery (some clients load tools on demand, e.g. Claude). The server returns the full tool list on every authenticated `tools/list`. Retry the same question once; if it keeps failing, Refresh/restart the connector and check 401/406 in troubleshooting above. |
| `rate_limited` / HTTP 429 / `busy` | You're sending too fast — read `Retry-After` / `X-RateLimit-*`, slow down, then retry. |
| Results say `truncated` | The page exceeded the size cap; use the returned `cursor` to fetch more. |

## Good to know

- **Read-only:** the server cannot modify any TimeDoctor data.
- **Companies:** call `td_list_companies`, then pass `company` per call. One
  OAuth connection can read every company that list returns. API keys stay on
  their configured company.
- **Limits:** ~10 requests/second, up to 500 rows per page, 256 KB per response,
  30-second timeout. See [`agent-guide.md`](./agent-guide.md) for details the agent uses.
- **Privacy:** responses are filtered to a fixed allowlist of fields; secrets/PII
  beyond what each tool documents are stripped server-side.
