Skip to main content
Glama
synchroteam-mcp

synchroteam-mcp

by kuzgunay

synchroteam-mcp

An open-source Model Context Protocol server that lets Claude (or any MCP client) work with Synchroteam field-service data: jobs, customers, sites, equipment, technicians and their schedules, invoices, quotations, parts and stock.

Status: unofficial, pre-release.

  • Not affiliated with, endorsed by or supported by Synchroteam.

  • Built from the public Synchroteam REST API v3 documentation (read 2026-11-08; links at the end).

  • Tested only against a local mock of that API (httpx.MockTransport, 157 tests).

  • Not yet tested against a live Synchroteam account. Expect small mismatches where the docs are ambiguous (listed under Known gaps) until someone runs it against a real tenant.

What you get

  • 21 read-only tools (19 lookups + 2 composite "business" tools), on by default.

  • 5 write tools (create job, add note, schedule, complete, cancel). They are not registered at all unless SYNCHROTEAM_ALLOW_WRITE=true.

  • Two transports from one codebase: stdio (Claude Desktop, Claude Code) and Streamable HTTP (remote clients).

  • Role-based access: a JSON file maps roles (e.g. engineer / sales / management) to tools. A tool outside the caller's role is not listed, and calling it is refused.

  • Bearer-token auth for HTTP: tokens map to roles, are stored as SHA-256 hashes, and a missing or unknown token gets 401.

  • Audit log: one JSON line per tool call with role, tool, sanitised arguments, result size, duration and outcome. No secrets, and no full personal data.

  • Careful API client: documented Basic auth, pagination, timeouts, retries with backoff that read the documented X-RateLimit-* / X-Quota-* headers, and clear error messages.

Built with the official MCP Python SDK 2.x. Its high-level API, MCPServer, is what 1.x called FastMCP.

Related MCP server: fieldroutes_mcp

Tools

Endpoints are relative to https://ws.synchroteam.com/api/v3. "Roles" refers to config/roles.example.json (management has *).

Tool

What it answers

Synchroteam endpoint(s)

Example roles

list_jobs

Jobs by status, date range, technician, team, customer, site, equipment, type

GET job/list

engineer, sales

get_job

One job in full: schedule, actual times, time spent, time entries, parts used, report summary

GET job/details

engineer

search_customers

Customers by name (wildcard) or email (exact)

GET customer/list

sales

get_customer

One customer: contact, VAT number, custom fields

GET customer/details

engineer, sales

list_sites / get_site

Customer sites (service locations)

GET site/list, GET site/details

engineer, sales

list_equipment

Installed equipment for a customer or site

GET equipment/list

engineer, sales

get_equipment

One machine plus its most recent jobs (quick service history)

GET equipment/details, GET job/list

engineer

list_users

Technicians, managers, admins: login, teams, skilled trades

GET user/list

management

list_teams

Teams with technician/manager counts

GET team/list

management

get_technician_schedule

One technician's jobs and activities (holiday, training, travel) with a per-day summary, up to 31 days

GET user/details, GET job/list, GET activity/list (x1-2)

engineer

list_invoices / get_invoice

Invoices by status/customer/job, and one invoice with line items

GET invoice/list, GET invoice/details

sales

list_quotations

Quotations by status/customer/job

GET quotation/list

sales

list_parts

Parts and services catalogue with stock level and a below-minimum flag; optional text search

GET part/list (several pages when searching)

engineer, sales

get_part

One part by id or exact reference

GET part/details

engineer, sales

list_stock_depots / get_stock_depot

Depots and vans, and the parts held in one

GET depot/list, GET depot/details

engineer

list_timeline

Notes, calls, emails and status notifications for a job/customer/site/equipment

GET timeline/list

engineer, sales

completed_jobs_not_invoiced

Completed/validated jobs in a date range with no invoice linked

GET job/list, GET invoice/list (+ optional per-job GET invoice/list)

management

customer_overview

Account summary: sites, equipment, open and recent jobs, unpaid invoices, open quotes

GET customer/details, site/list, equipment/list, job/list x2, invoice/list x3, quotation/list x2

sales

create_job (write)

Create an unscheduled job for a customer

GET job/details (myId check), POST job/send

-

add_job_note (write)

Add a timeline note to a job

(GET job/details), POST timeline/send

engineer

schedule_job (write)

Assign technician and time slot

POST job/schedule

-

complete_job (write)

Mark completed with actual start/end

POST job/complete

engineer

cancel_job (write)

Cancel a job

POST job/cancel

-

List tools return compact summaries plus page, total, has_more, next_page and a hint when more results exist (page_size is capped at the documented maximum of 100). Every tool carries MCP annotations (readOnlyHint, destructiveHint, ...), and every write tool says "WRITES TO SYNCHROTEAM" in its description.

The two business tools, and their limits

completed_jobs_not_invoiced(date_from, date_to, ...): the documented job object has no "invoiced" flag. The tool lists completed (and, by default, validated) jobs in the range, then scans invoices created since date_from - invoice_lookback_days (default 30), ignoring cancelled ones. It matches the two through each invoice's job reference (id, num or myId). It returns the uninvoiced jobs together with caveats covering:

  • invoices created without a job link (it reports how many it saw);

  • quotations and credit notes, which are not considered;

  • truncation caps (max_jobs, a 5000-invoice scan).

verify_each_job=true adds one invoice/list?job_num= lookup per candidate (at most 100). That also catches invoices created outside the scan window.

customer_overview(customer_id | customer_my_id | customer_name): about 10 API calls. Invoice totals are summed over at most 100 invoices per status. The API exposes no currency field, so amounts have no currency. Invoices and quotations are queried once per status, because the docs do not say whether their status filter accepts several values.

Setup

Requires Python 3.11 or newer.

git clone <this repo> synchroteam-mcp && cd synchroteam-mcp
python -m venv .venv
# Windows: .venv\Scripts\activate    macOS/Linux: source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env    # then fill in the placeholders, or set the variables in your MCP client config
synchroteam-mcp check-config

Where the credentials come from (according to the Synchroteam docs):

  • Domain: the mycompany part of https://mycompany.synchroteam.com.

  • API key: in the Synchroteam web app under Configuration > Authentication Key.

Configuration

Variable

Default

Meaning

SYNCHROTEAM_DOMAIN

(required)

Your Synchroteam domain identifier

SYNCHROTEAM_API_KEY

(required)

API key (Configuration > Authentication Key)

SYNCHROTEAM_BASE_URL

https://ws.synchroteam.com/api/v3

API base. http:// is accepted only for localhost (the fake API)

SYNCHROTEAM_ALLOW_WRITE

false

Register the write tools

SYNCHROTEAM_TIMEOUT / SYNCHROTEAM_MAX_RETRIES

30 / 3

Per-request timeout (s) and retry budget

SYNCHROTEAM_MCP_ROLES_FILE

(none)

JSON roles file. Without it, a single role full sees every registered tool

SYNCHROTEAM_MCP_ROLE

full if no roles file

Role used by the stdio transport (required when a roles file is set)

SYNCHROTEAM_MCP_TOKENS_FILE

(none)

HTTP: JSON file of {name, role, sha256} token entries

SYNCHROTEAM_MCP_TOKENS

(none)

HTTP: role:token,role:token (tokens of 24 characters or more), e.g. from a secret store

SYNCHROTEAM_MCP_AUDIT_LOG

(none)

Path of the JSON-lines audit log

SYNCHROTEAM_MCP_HOST / _PORT / _PATH

127.0.0.1 / 8000 / /mcp

HTTP bind address and endpoint

SYNCHROTEAM_MCP_ALLOWED_HOSTS

(none)

Comma-separated public hostnames. Turns on Host/Origin checking when serving behind a domain

SYNCHROTEAM_MCP_STATELESS / _JSON_RESPONSE

true / true

Stateless Streamable HTTP with plain JSON responses (load-balancer friendly)

SYNCHROTEAM_MCP_LOG_LEVEL

INFO

Log level (logs go to stderr)

Use with Claude Desktop (stdio)

Edit claude_desktop_config.json (Settings > Developer > Edit Config):

{
  "mcpServers": {
    "synchroteam": {
      "command": "C:\\path\\to\\synchroteam-mcp\\.venv\\Scripts\\synchroteam-mcp.exe",
      "args": ["--transport", "stdio"],
      "env": {
        "SYNCHROTEAM_DOMAIN": "your-domain",
        "SYNCHROTEAM_API_KEY": "your-api-key",
        "SYNCHROTEAM_MCP_ROLES_FILE": "C:\\path\\to\\synchroteam-mcp\\config\\roles.example.json",
        "SYNCHROTEAM_MCP_ROLE": "management",
        "SYNCHROTEAM_MCP_AUDIT_LOG": "C:\\path\\to\\synchroteam-mcp\\logs\\audit.jsonl"
      }
    }
  }
}

On macOS/Linux, the command is /path/to/synchroteam-mcp/.venv/bin/synchroteam-mcp.

Use with Claude Code

# Local, stdio
claude mcp add synchroteam \
  --env SYNCHROTEAM_DOMAIN=your-domain \
  --env SYNCHROTEAM_API_KEY=your-api-key \
  -- /path/to/.venv/bin/synchroteam-mcp --transport stdio

# Remote, Streamable HTTP with a bearer token
claude mcp add --transport http synchroteam https://mcp.example.com/mcp \
  --header "Authorization: Bearer <token issued to you>"

Remote deployment (Streamable HTTP)

synchroteam-mcp hash-token          # prints a new random token (give it to the user) and a tokens-file entry
synchroteam-mcp --transport http    # serves http://127.0.0.1:8000/mcp, plus GET /healthz without auth

Docker (non-root user, /healthz health check):

docker build -t synchroteam-mcp .
docker run --rm -p 8000:8000 --env-file .env \
  -e SYNCHROTEAM_MCP_ROLES_FILE=/etc/synchroteam-mcp/roles.example.json \
  -e SYNCHROTEAM_MCP_TOKENS_FILE=/run/secrets/tokens.json \
  -v "$PWD/tokens.json:/run/secrets/tokens.json:ro" \
  synchroteam-mcp

The Dockerfile has not been built in the development environment (Docker was not available there). Only the Python wheel it installs was built and checked.

Deployment notes:

  • Put TLS in front (reverse proxy, load balancer or platform ingress). The server speaks plain HTTP.

  • Set SYNCHROTEAM_MCP_ALLOWED_HOSTS=mcp.example.com when serving on a public hostname. This turns on the SDK's Host/Origin validation; it is automatic only when bound to localhost.

  • Stateless mode (the default) needs no sticky sessions, so you can run several replicas behind a load balancer.

  • One server instance uses one Synchroteam API key. Roles restrict which tools a caller can use, but every call runs with that key's permissions inside Synchroteam.

OAuth (what Claude.ai custom connectors prefer) is not implemented

This server authenticates HTTP callers with static bearer tokens. That works for Claude Code, scripts, the MCP Inspector and other clients that can send an Authorization header. Claude.ai custom connectors are built around the MCP authorization spec (OAuth 2.1), so this server is not yet a drop-in Claude.ai connector. Check Anthropic's current connector documentation for the exact requirements.

Adding OAuth would take:

  1. An authorization server (Microsoft Entra ID, Auth0, Keycloak, ...) that issues access tokens for this server as an audience/resource, ideally with dynamic client registration or a pre-registered client for Claude.

  2. A TokenVerifier implementation. The MCP SDK provides the hook (MCPServer(token_verifier=..., auth=AuthSettings(...))). It would validate the JWT signature, issuer, audience and expiry, and map a group/role claim onto the roles file.

  3. Publishing Protected Resource Metadata (RFC 9728). The SDK serves it from AuthSettings.

  4. Replacing BearerAuthMiddleware with the SDK's auth middleware. The role and audit layers stay unchanged.

Try it without a Synchroteam account (fake API + MCP Inspector)

python scripts/fake_synchroteam.py   # fake API on http://127.0.0.1:8765/api/v3 with fictional data

# second shell (bash syntax; use `set`/`$env:` on Windows)
export SYNCHROTEAM_BASE_URL=http://127.0.0.1:8765/api/v3
export SYNCHROTEAM_DOMAIN=acme-test
export SYNCHROTEAM_API_KEY=00000000-test-key-not-real-0000   # placeholder accepted only by the fake
npx @modelcontextprotocol/inspector .venv/bin/synchroteam-mcp --transport stdio

The Inspector opens a browser UI where you can list tools and call them. To inspect the HTTP transport, start synchroteam-mcp --transport http with SYNCHROTEAM_MCP_TOKENS=full:<a 24+ character token>. Then choose "Streamable HTTP" in the Inspector, use http://127.0.0.1:8000/mcp and add an Authorization: Bearer ... header.

Security model

  • Read-only by default. Write tools are only registered when SYNCHROTEAM_ALLOW_WRITE=true. When it is off, they are absent from tools/list and cannot be called by any role. create_job refuses to reuse an existing myId, because Synchroteam's job/send would otherwise silently update that job (the documented upsert).

  • Roles. Enforced in an MCP server middleware for every transport. The role filters tools/list and refuses tools/call with a protocol error before the API is touched. The check fails closed: an HTTP request without an authenticated principal sees no tools. Tool names in the roles file are validated at startup, so a typo does not silently hide a tool.

  • HTTP authentication. Authorization: Bearer is checked in plain ASGI middleware before the MCP SDK runs. Tokens are compared as SHA-256 digests with hmac.compare_digest. The tokens file holds only hashes and is rejected if it contains plaintext. tools/list results are marked cache scope private.

  • Audit. One JSON line per call:

    {"ts":"2026-11-08T17:20:01.123Z","event":"tool_call","transport":"http","role":"sales","principal":"sales-team",
     "tool":"get_customer","arg_keys":["customer_email"],"args":{"customer_email":"<redacted:28>"},
     "outcome":"ok","error":null,"result_bytes":1834,"duration_ms":212.4}
    • Argument values are kept only for ids, enums, dates and paging; names, emails and free text become length markers.

    • Results are never written, only their size.

    • Refused calls are logged with outcome: "denied".

  • Secrets.

    • The API key and tokens come only from the environment or files outside git (.env, tokens.json and config/roles.json are git-ignored).

    • They are excluded from repr().

    • The base URL must be https:// unless it points at localhost.

  • Log hygiene.

    • All logging goes through a redacting filter that masks emails and phone numbers and strips Basic/Bearer credentials and the API key.

    • httpx request logging (which would print query strings such as email filters) is turned down to WARNING.

  • Data minimisation.

    • User password fields are always stripped. The docs show the field in the user object example, although they call it write-only.

    • Technician GPS (lastLocation, startLocation) is not returned.

  • Error messages tell the model what to fix ("No job found with num=99999", "rate limit reached", "check SYNCHROTEAM_API_KEY") without echoing credentials.

Known gaps and doc ambiguities

Not verified against a live account. The points below come from reading the docs, and each is handled in the code as described:

  1. Base path. Resource examples use /api/v3/..., the auth example /Api/v3/, and the pagination/error examples /v3/job/list. The default is /api/v3; override it with SYNCHROTEAM_BASE_URL.

  2. Rate limits. The text says 1000 requests/minute, while the error example shows X-RateLimit-Limit: 120. The daily quota appears as 15000 in one example and 10000 in another. The client reads the headers instead of assuming a number. It treats a 429 with X-Quota-Remaining: 0 as the daily quota (not retried, resets at UTC midnight), inferred from the docs' example. Retry-After is not documented but is honoured if present.

  3. Date formats.

    • Job object examples use 19/07/2016 14:43:00 (day/month/year); other objects use 2018-04-06 10:19. Whether this depends on account locale is not stated. The server parses both and always returns the original string.

    • Job dateFrom/dateTo filters do not state a time zone. changedSince-style filters are documented as UTC, and activities as local time.

  4. customer/list. The parameter table says changedSince, but the example URL uses changedFrom. The server uses changedSince.

  5. activity/list.

    • user_login is typed "integer", apparently a typo. The server filters by user_id.

    • The category filter lists unavailability, while the category code is unavail. The server filters by category client-side and accepts both.

  6. Job reports. The block title is name in one example and blockName in another. Both are read. The report embedded in job/details is used; if it is missing, get_job says so rather than guessing.

  7. job_id filters typed as integer on invoice/list, quotation/list and timeline/list, although job ids are strings such as 265_160719144323649. The invoice and quotation tools expose job_num / job_my_id instead. list_timeline passes job_id as given, which is untested live.

  8. Multi-value status. It is documented for job/list but not for invoices or quotations, so those are queried one status at a time.

  9. Depot ids. Typed int, but the example is a GUID. They are treated as strings.

  10. Timeline.

    • Timeline (and tasks) lists are documented as bare arrays with no pagination parameters. list_timeline caps output client-side.

    • The timeline create example uses client for the customer while the object uses customer.

    • Whether user is required on timeline/send is not stated; add_job_note sends only the job reference (by id, resolving num/myId first because the example only shows id).

  11. pageSize above 100. The behaviour is undocumented, so the client never sends more than 100.

  12. Missing data. The job object has no invoice flag, invoices carry no currency, and there is no working-hours endpoint. get_technician_schedule therefore reports booked jobs and unavailability, not computed free slots.

  13. API access by plan. The Synchroteam help-centre article on web services (https://support.synchroteam.com/hc/en-us/articles/360015843091-Web-services) returned a Cloudflare bot check (HTTP 403) and was not read. Which subscription plans include API access is therefore unconfirmed.

  14. Error bodies. They are assumed to follow the documented {"error", "message"} shape. Other shapes are tolerated and reported with the HTTP status.

Before testing against a live Synchroteam account

  1. Use a Synchroteam account with API access, ideally a trial or sandbox tenant rather than production. Confirm with Synchroteam that your plan includes web services.

  2. Copy the domain and the key from Configuration > Authentication Key.

  3. Start read-only (the default) and run synchroteam-mcp check-config. Then try small calls: list_teams, list_jobs with page_size: 1, then get_job.

  4. Check the items listed above, especially the date formats in responses, the rate-limit headers, the timeline/list job_id filter, the job field on invoices (which completed_jobs_not_invoiced relies on) and whether the job report is embedded in job/details.

  5. Enable SYNCHROTEAM_ALLOW_WRITE=true only on a test tenant, and try each write tool once.

Development

pip install -e ".[dev]"
pytest              # 157 tests; no external network (end-to-end tests use localhost only); ~10 s
  • src/synchroteam_mcp/client.py: HTTP client (auth, pagination, retries, error mapping)

  • src/synchroteam_mcp/tools/: read.py, business.py, write.py

  • src/synchroteam_mcp/middleware.py: bearer auth (ASGI), role filter and audit (MCP middleware)

  • src/synchroteam_mcp/testing/fake_api.py: fake of the documented API used by the tests and the demo script

  • tests/:

    • client behaviour (auth header, pagination, 429/5xx retry and backoff, error mapping)

    • every tool over an in-memory MCP session

    • write-flag behaviour

    • role filtering

    • HTTP bearer checks via the SDK client

    • audit and log redaction

    • end-to-end runs of the real CLI over stdio and HTTP

Documentation used

License

MIT, see LICENSE. "Synchroteam" is a trademark of its owner and is used here only to describe compatibility.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Self-hosted, open-source (AGPL-3.0) MCP gateway that turns REST, SOAP/WSDL, GraphQL, and SQL/NoSQL databases into MCP tools. Imports OpenAPI/Postman/WSDL/GraphQL specs and bridges multiple MCP servers behind one endpoint. Ships 188 pre-built adapters, 26 of which need no API key, plus per-tool response mapping, OAuth2/RBAC/SSO and a full audit log.
    4
    756
    AGPL 3.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server for the FieldRoutes pest-control / lawn-care operations platform — talk to your data from Claude, Cursor, or any MCP client.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    An MCP server that gives agents safe, policy-scoped access to your database via structured queries (never raw SQL), with read and opt-in write support.
    12 npm
    5
    MIT
  • F
    license
    A
    quality
    C
    maintenance
    Enables users to connect to Salesforce and perform CRUD operations, run SOQL/SOSL queries, and manage records through a lightweight MCP server with built-in safety gates for writes.
    9
    1
    -