Skip to main content
Glama
dragosh29

WorkMobile MCP server

by dragosh29

WorkMobile MCP server

An MCP server that lets Claude, ChatGPT and other MCP clients work with a WorkMobile account: jobs dispatched to field staff, job types, forms and the records submitted on them, approvals and mobile users, and (when enabled) creating and allocating jobs and approving or rejecting records. It is built from WorkMobile's public documentation only: the Web API v2 OpenAPI spec, the WorkMobile help centre's API 2.0 articles, and the Power Automate connector definition that eSAY Solutions Ltd publishes (the source of the response schemas; the v2 spec has none for these endpoints).

Once it's connected, someone in the office can ask things like:

  • "Which jobs are booked for Sam tomorrow, and which are still unallocated?"

  • "Show me the history of job 70001."

  • "What came in on the Gas Meter Inspection form this week where the meter type was Gas or Water?"

  • "What's waiting for my approval on the Customer Visit Record form?"

  • With writes enabled: "Raise a meter read job at 3 Example Lane for Friday and give it to Sam", "Approve record 30000 on the inspection form."

Tools

Tool

What it does

API calls

list_jobs

Searches jobs by status IDs, estimated start date range, the mobile users they are allocated to, job types, description, priority, backlog and whether a duration is set; optional sort by estimated start. Pages of 500 (the smallest page size the API allows). A POST that only reads.

POST /api/jobs/search

get_job

One job: description, status, type, priority, dates, allocation, history (status changes with narrative) and job data.

GET /api/jobs/{id}

list_job_statuses

WorkMobile's static list of job status IDs, returned as WorkMobile sends it (the format is not documented).

GET /api/jobs/jobstatuslist

list_job_types

Job types with the form each uses, default duration and location settings.

GET /api/jobtypes

get_job_type

One job type and the fields a job of that type takes (name, type, required, allowed values, limits), read from its JSON Schema document.

GET /api/jobtypes/{id}, GET /api/jobtypes/{id}/schema

list_forms

Forms visible to the API user (sub-forms hidden unless asked). The API returns the whole list without paging.

GET /api/forms

list_form_submissions

Searches the records submitted on one form by created or uploaded date, job, mobile user, user group, or searchable field values with the documented operators; newest first by default. Pages of 500. A POST that only reads.

POST /api/forms/{FormId}/completedrecords/search

get_form_submission

One record by its Id (the search's id criterion).

POST /api/forms/{FormId}/completedrecords/search

list_approvals

Without form_id: the API user's approval inbox totals, returned as sent (format not documented). With form_id: the records on that form pending the API user's approval (onlyMyPendingApprovals).

GET /api/approvals/getinboxtotals or the records search

list_mobile_users

Field staff with name, job title, user group name and whether active (inactive users hidden unless asked).

GET /api/mobileusers, GET /api/usergroups

create_job

Raises a job of one type, optionally allocated to one mobile user. Fetches the job type's schema first and refuses locally when a data field is unknown, a required field is missing, a value is not an allowed one, a numeric field gets text, or only one of latitude/longitude is given. Only registered when writes are enabled.

GET /api/jobtypes/{id}/schema, POST /api/jobs (multipart form)

allocate_job

Allocates or reallocates a job to one mobile user, replacing any previous allocation. Marked destructive. Writes only.

POST /api/jobs/{JobId}/allocate/{MobileUserId}

approve_record

Approves the current approval step of a record as the API user. Cannot be undone. Marked destructive. Writes only.

POST /api/approvals/approve

reject_record

Rejects a record's current approval step. Rejection is terminal: the workflow stops for good. Marked destructive. Writes only.

POST /api/approvals/reject

The v2 API is the whole web application's API (158 operations). Not covered on purpose: the username/password login (/api/account/authenticate) and every other account, password, SSO and API-token route; CRM customers and contacts and lone worker event logs (their read endpoints exist, but WorkMobile publishes neither a response schema nor an example for them, so their field names would have to be guessed); record revision and approval history (same reason); reports, media attachments, resources and every other download; track-worker locations; notification history; filtered views, menus and push notifications; user (portal login) management; job editing, closing, unallocation, group broadcast and routing; and every delete endpoint.

Related MCP server: Amiqus MCP server

Setup

Requires Node 18 or later.

npm install
npm run build

You need an API token for your WorkMobile account. WorkMobile's help centre recommends it for unattended use: create a portal user with suitably limited access on the Logins page, then click Generate in its API Token section and copy the GUID. The token gives non-expiring access until it is revoked or regenerated. The server sends it in the X-API-Key header on every request; it never uses a username or password.

Claude Desktop: add this to claude_desktop_config.json:

{
  "mcpServers": {
    "workmobile": {
      "command": "node",
      "args": ["/absolute/path/to/workmobile-mcp/dist/index.js"],
      "env": { "WORKMOBILE_API_KEY": "your-api-token" }
    }
  }
}

Claude Code:

claude mcp add workmobile -e WORKMOBILE_API_KEY=your-api-token -- node /absolute/path/to/workmobile-mcp/dist/index.js

Variable

Required

Meaning

WORKMOBILE_API_KEY

yes

The API token (a GUID) of a portal user, sent as the X-API-Key header.

WORKMOBILE_ALLOW_WRITES

no

true to register create_job, allocate_job, approve_record and reject_record. Off by default.

WORKMOBILE_BASE_URL

no

Defaults to https://www.esayworkmobile.co.uk/webapi2. Used by the tests; an on-premise instance would set its own. Must not contain a username or password.

Safety defaults

  • Read-only unless WORKMOBILE_ALLOW_WRITES=true. Every read tool carries the MCP readOnlyHint annotation, including the ones that call a search endpoint with POST (list_jobs, list_form_submissions, get_form_submission, and list_approvals with a form_id): WorkMobile runs its job and record searches as POST requests with a criteria body, and they change nothing. allocate_job (it replaces the job's current allocation), approve_record and reject_record (they move an approval workflow on for good) carry destructiveHint: true, since MCP reserves false for tools that only add; create_job, which only adds a job, is a write that is not destructive.

  • Personal data is withheld by default and returned only when a tool is called with include_contact_details=true:

    • Job data and form answers are keyed by the account's own field names, so fields are classified by name. A field whose name mentions an email, phone, mobile or "mob", postcode, address, street, what3words, customer, client, tenant, resident, patient, witness, next of kin, name (any "name", so "Site Name" too), signature, photo, image, sketch, video, audio, attachment or file, location, GPS, latitude/longitude, date of birth (including "DoB" and "D.O.B"), passport, NHS or national insurance number (including "NINumber" and "NINo"), vehicle registration, injury, medical, medication, allergy, disability, illness or diagnosis, salary, wage, earnings, overtime, pay, paid or rate, or contact/person/owner/driver is replaced with [withheld: personal data; …]. Names are matched in any style: PascalCase unique names as WorkMobile's documentation writes them ("MeterType"), acronyms ("GPSCoords"), and labels with spaces, dots or hyphens. "Health" alone is not a trigger, because "Health and Safety" fields are common in field-service forms. Nested values are checked the same way.

    • A job's Location (its address) is withheld; a mobile user's Username (often an email address) is not returned.

    • In every other text (job descriptions and history narratives, answers in fields with ordinary names, names and job titles, form descriptions, job type names, WorkMobile's own error messages, the job status list, the approval inbox totals) email addresses become [email redacted], phone-number-like sequences [phone redacted], latitude/longitude pairs [location redacted] and UK postcodes [postcode redacted]. The phone match is the same heuristic as the other prototypes: international numbers with + or 00, UK numbers with a bracketed area code, and UK-style 0… numbers of 9 to 11 digits; other digit strings starting with 0 are caught too. The postcode match is upper case only. People's names and street addresses written in free text are not recognised: "lives at 14 Example Road" comes back as written (only a postcode after it is redacted), and so does a customer's name in a job description or history narrative.

    • A value that is a bare link (http… or www.…) in job data or form answers is withheld, because it may point to a photo, signature or file.

    • Staff names are returned: mobile users' names, CreatedBy on records and job history, and the approval columns of a record. A record's Customer column, like any field with "customer" in its name, is withheld. A system column (such as CreatedBy or UserGroup) or a job Location that arrives as an object or an array rather than text is walked like job data: its keys are classified by name and its text redacted.

  • Never returned, whatever is asked: fields whose name mentions a bank, sort code, IBAN, SWIFT/BIC, account number ("Acc No", "Acct"), card, CVV/CVC, expiry, PAN, last 4 or last four, or payment; any 13-to-19-digit number that passes the Luhn check in any text (replaced with [card number redacted]); in any text, an IBAN that passes its mod-97 check, and a sort code, account number or card's last four digits when the words in front name them ("sort code 12-34-56", "acct 12345678", "card ending 4242") (replaced with [bank details redacted]; a bare 6- or 8-digit number is left alone); and any embedded file (a data: URI, as a signature or photo could be sent) (replaced with [embedded file omitted; files are never returned]). No file is ever downloaded: the server never calls the report, media, resource or download endpoints.

  • IDs are checked before any call is made: every ID must be a whole number from 1 to 2147483647 (the spec types them as int32, except FormId in the path of the records search, which it types as a string; form IDs are int32 everywhere else). Dates take YYYY-MM-DD (real calendar dates only; an impossible month or day is refused with "Not a real calendar date"), which becomes the start of that day in UTC for a "from" and the end for a "to", or an ISO 8601 date-time with a zone (Z or an offset) and an hour from 00 to 23, as in WorkMobile's documented examples. Field filter operators must be one of the eleven the search documentation lists.

  • WorkMobile does not document a rate limit (nothing in the v2 spec, the connector definition or the help centre's API 2.0 articles). Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice for any method, on the assumption that a rate-limited request was not processed, waiting for Retry-After (whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). Each wait is capped at 10 seconds; if WorkMobile asks for a longer wait the call gives up at once and the message says how long to wait. A tool call that makes several requests (up to 10 pages) can still run past the MCP client's default 60-second request timeout.

  • 502, 503 and 504 are retried the same way for GET only; when all three attempts fail the error says the service may be unavailable, without the gateway's HTML. No POST is ever retried after a gateway error. For the searches the message says the search only reads, so calling the tool again is safe; for a write it says the request may already have been processed and names the tool to check with (list_jobs, get_job or list_approvals) before repeating it.

  • A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming WORKMOBILE_BASE_URL, never as an empty list; the body is described by type and size, never quoted. A search that answers with anything other than a JSON array of rows is reported with the key names it got, never read as empty. The connector definition types the job search answer and the job type schema document as strings, so a JSON array or object sent double-encoded (as a JSON string holding JSON text) is decoded once; any other string is still refused. A single-record GET answering 200 with null or an empty object is reported as not found.

  • A rejected API key (401) produces a message that says which variable to fix and where the token comes from; a 403 names the roles WorkMobile's documentation lists for the API user; a 400 passes on WorkMobile's title, detail and field errors when the body is ProblemDetails JSON. The API key is scrubbed from every error message, and contact details are redacted from WorkMobile's error text.

Tests

npm test

The spec (spec.json, from https://www.esayworkmobile.co.uk/webapi2/swagger/v2/swagger.json) and the connector definition (connector.json, from microsoft/PowerPlatformConnectors, certified-connectors/WorkMobile/apiDefinition.swagger.json, publisher eSAY Solutions Ltd) are downloaded on the first run if they are missing. Both are gitignored.

Where the schemas come from:

  • Request bodies: the v2 spec's own component schemas (JobSearchCriteria, CompletedRecordsSearchCriteria, ApproveRequest, RejectRequest, all with additionalProperties: false) and its multipart schema for POST /api/jobs. One substitution: the spec's UserDefinedFieldFilter is an object with no properties and additionalProperties: false, so it accepts only {} and would reject every documented filter example; filter items are validated against the spec's UserDefinedFieldFilterDefinition (uniqueName, operator, value, …) instead, and record search bodies without filters are validated against the unmodified schema.

  • Responses of forms, job types, jobs, mobile users and user groups: the connector definition's response schemas. The help centre's documented GET /api/Forms example shows "LastUpload": null where the connector types a string, so that one field is allowed to be null.

  • Job search rows: WorkMobile documents the response only as "the list of jobs that meets the criteria". The mock answers a bare JSON array of objects shaped like the connector's GET /api/Jobs/{id} schema; this is an assumption.

  • Completed records: WorkMobile publishes neither a schema nor an example. test/schemas.mjs holds a schema written by hand from what is documented (the numeric Id, OriginalId and JobId columns the search documentation filters and sorts on, and the Created and CreatedBy static fields of the documented form example), with every other key taken to be one of the form's fields by unique name. The whole row shape, and the bare-array envelope, are assumptions.

  • Job type schema document: the help centre's sample, copied verbatim (test/doc-examples.mjs) and served as job type 10238; the two documented GET /api/Forms examples are served verbatim too.

  • Job status list, approval inbox totals, error bodies, write replies: not documented; the mock's placeholders are listed at the top of test/mock-server.mjs. The tools that return the first two pass them through without relying on any field name.

The test suite (28 checks):

  1. Validates every fixture against those schemas, reports any fixture key the connector schema does not declare, confirms the documented examples are served unchanged, and runs negative controls (a string JobId, a record without OriginalId, an undeclared key). Checks the field-name classifier on its own against personal, financial and ordinary names in PascalCase, acronym, dotted and spaced forms, and the bank-detail text patterns.

  2. Starts a local mock of the API under /webapi2 that requires the X-API-Key header (401 "Unauthorised" without it or with a wrong key), serves 1,100 jobs and 620 records on one form in pages of 500 with the documented pagination body (refusing a rowCount below 500, and a record search without orderBy, which the help centre says is always required), answers unknown IDs with a 404 and bad requests with a 400 as ProblemDetails, and answers the first GET /api/forms with a 429. The mock's list and detail responses are validated against the schemas above.

  3. Starts the built server and drives it over stdio with the official MCP client: tools/list and annotations; every read tool; job search pagination across three pages to the short last page, continuation from next_page, and the note when rows of a fetched page are not returned; every documented job search filter and the record search criteria (created and uploaded dates, job, mobile user, user group, field filters with AND/OR, sort order) sent exactly as documented, in bodies validated against the spec; redaction by default (in list_jobs, get_job, list_form_submissions, get_form_submission, list_approvals with a form_id, list_mobile_users, list_forms and the approval inbox totals: personal fields by name, including DoB, D.O.B, NINumber, What3Words and Mob No, a bare link, emails, phone numbers, postcodes and coordinates in text) and its return on request (in get_job, get_form_submission, list_approvals and list_mobile_users), with bank and card fields, a Luhn-valid card number, a sort code, an account number and an IBAN typed into text, and an embedded image withheld even on request; object- and array-valued system columns and an object Location redacted like job data; a double-encoded job type schema (read by get_job_type and used by create_job) and job search answer decoded; the write tools absent with WORKMOBILE_ALLOW_WRITES unset and set to false; create_job posting a multipart form whose fields are all in the spec's schema and validate against it, with Data as {"jobData": …}, and refusing unknown fields, missing required fields, disallowed values, text for a numeric field and half a location before any POST; allocate_job, approve_record and reject_record bodies validated against the spec, and a 400 passed on; invalid IDs, dates (including month 13, 30 February and hour 24, with the validation message checked) and operators refused with no request made; the 404 message for unknown job and job type IDs and a 200 null or {} job read as not found; requests of a paged search spaced by the 250 ms throttle, and paging stopping once max_results rows are in hand; the 429 retry waiting for Retry-After in the seconds, fractional-seconds and HTTP-date forms and the 2 s fallback when the header is absent, retrying a POST with the same body (page 2 of a three-page search asked for again with no row lost or duplicated, and a rate-limited POST /api/jobs creating exactly one job), giving up after three attempts on a persistent 429 and at once on a Retry-After above the cap; a 502 retried for a GET and never for a POST search or write, with the right advice in each case; three 503s on a GET reported without HTML; a non-JSON 200, a search answer that is not an array, and a 403 (whose body echoing the API key and an email address reaches the tool result scrubbed); the 401 message for a wrong key, after exactly one request; and, last, that every request carried X-API-Key (the configured one, except the deliberate wrong-key call) and no Authorization header, matched exactly one method and path documented in the spec (integer path parameters match digits only, so a parameter cannot stand in for a literal segment), sent a body content type the spec accepts for that operation, and never touched a login, download, media or report endpoint.

The suite takes about 35 seconds.

Status

This is a working prototype. It has not been run against the live API, because it was built without a WorkMobile account (trials are set up through WorkMobile's sales team). Everything below comes from the published documentation and should be confirmed on a real account:

  • The response of POST /api/jobs/search and POST /api/forms/{FormId}/completedrecords/search: that each page is a bare JSON array (not an object with the rows and a total, and not the JSON string the connector definition types the job search answer as; a double-encoded array is decoded, but that path has only been tried against the mock), that job rows carry the same fields as GET /api/jobs/{id}, and that a page with fewer than 500 rows is the last. If the envelope differs, the server says so with the key names it got rather than guessing.

  • The shape of a completed record: that Id, OriginalId and JobId are columns with those names, which other system columns exist and what they are called, and that the form's fields are keyed by unique name with scalar values. Also how photos, signatures, sketches and locations appear in a record (a link, an ID, embedded data), so the name- and value-based withholding can be checked against real data.

  • Whether the record search criteria are sent as arrays (createdDateFrom: ["…"], id: [21524]), as the current spec types them, or as the single values the 2021 help centre article shows; and how userDefinedFilter items are really read, given the spec's empty UserDefinedFieldFilter schema.

  • Which time zone WorkMobile applies to the search dates. The server sends UTC (…Z), as the documented examples do; the timestamps in the documented responses carry no zone.

  • Whether GET /api/jobtypes/{id}/schema returns the JSON Schema document as an object (as the help centre sample shows) or as a JSON string holding it (as the connector types it); both are handled, only against the mock. And whether a job's Location is ever an object rather than the connector's string.

  • What the API answers for an unknown job, job type or form ID (the spec lists only 200 and 401 for these GETs); the server handles a 404 and a 200 with null or {}.

  • The format of GET /api/jobs/jobstatuslist and GET /api/approvals/getinboxtotals (returned as sent, with text redaction), and the IDs of the nine documented statuses.

  • The error bodies: whether 400s come as ProblemDetails, and whether 401 and 403 carry anything. What a 403 looks like for a portal user that lacks a role.

  • POST /api/jobs: that Data is the JSON document {"jobData": {…}} (the documented JSON Schema describes an object with a jobData property), that AllocatedMobileUserId and AllocatedUserGroupId of 0 mean unallocated (as the connector says), what Priority values and Duration unit are valid, that EstimatedStart accepts a UTC date-time, and that the reply is the new job ID as an integer (as the connector types it). The local check of job data covers only unknown fields, required fields (marked, as in the documented sample, by a required array holding the field's own name), allowed values and numeric types; WorkMobile's own validation is the authority.

  • The replies of allocate (typed as a string by the connector), approve and reject (documented only as "Success"), and what approve/reject answer for a record with no step pending for the API user.

  • Whether a 429 on a POST means the request was not processed (the retry assumes so), and how many requests per second the API tolerates; nothing is documented, so the 250 ms spacing is a guess on the polite side.

  • The roles the API user needs for each tool; the 403 message quotes role names from the example token in WorkMobile's authentication article.

Going to production

This version runs locally over stdio, with the account holder's own API token. For customers to connect from claude.ai or ChatGPT without handling tokens, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by eSAY Solutions, and then a listing in the Claude and ChatGPT connector directories. A production version would also cover what this one leaves out for lack of documented response shapes (CRM customers and contacts, lone worker events, record history), once those shapes are confirmed.

Licence

MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

  • Give AI assistants secure access to your organization's structured business data. Search records, create and update records, retrieve schema information, and manage workflow states using natural language. You need two values for every request: x-api-key — your Web Data Forms API Key x-group-id — your Web Data Forms Group ID You can find these in your Web Data Forms accounts group->information page. Preferred method: request header When possible, pass the credentials as HTTP headers: x-api-key: <your-api-key> x-group-id: <your-group-id> This is the preferred option because it keeps credentials out of the URL and is more secure. Fallback method: query parameters If your MCP client does not support custom headers, the server also accepts the credentials as URL query parameters. Example: https://mcp.webdataforms.com?x-api-key=abc123&x-group-id=xyz456 Detailed information here: https://github.com/Web-Data-Forms/mcp-server-docs/blob/main/README.md

  • Let AI agents query data and act across all your business apps via MCP.

  • MCP server that lets AI assistants use all OneSchema features exposed via the public API.

  • Connect any AI agent to 1,000+ apps and 27,000+ actions through one remote MCP server (OAuth).

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to read and manage a Jobkeepr field service business including jobs, customers, scheduling, estimates, invoices, and payments via MCP.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables Claude, ChatGPT and other MCP clients to read an Amiqus ID account—clients, onboarding records and steps, check results, templates, case status counts and webhooks—and, when writes are enabled, create records.
    9
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables Claude, ChatGPT and other MCP clients to read practice-management data including organization, clinicians, diaries, availability, bookings, patients, invoices, payments, staff tasks, services, and locations, and optionally create staff tasks, create bookings, and cancel bookings.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Lets MCP clients such as Claude and ChatGPT read a rota and time-and-attendance account, exposing venues, groups, shifts, absences and absence types, time entries, venue events and staff names through read-only tools that never return pay data.
    MIT