mcp-jobber
v0.1.0
Published
Secure, multi-tenant Model Context Protocol server for the Jobber field-service platform.
Maintainers
Readme
Jobber MCP Server
A secure, multi-tenant Model Context Protocol (MCP) server for the Jobber field-service platform. It lets AI assistants read and act on a Jobber account, clients, jobs, quotes, invoices, and scheduling, through a small set of tools designed for safe, correct use. It runs over stdio for local use and over an authenticated, multi-tenant Streamable HTTP transport for hosted use.
Unofficial and community-built. This project is not affiliated with or endorsed by Jobber. Jobber's App Marketplace does not list MCP integrations, so this is a self-hosted reference implementation: you run it against your own Jobber developer app and account.
Why this one is different
Most public MCP servers ship with no authentication, unvalidated inputs, and no guardrails on destructive actions. This one is built the opposite way, security first:
- OAuth 2.0 with a correct token lifecycle (refresh, expiry, rotation), no long-lived hardcoded keys.
- Every tool input and every Jobber API response validated with
zod. - Least-privilege OAuth scopes, documented and justified.
- Financial and data-changing tools guarded by a preview-by-default (
dryRun) plus explicitconfirm. - No secrets, tokens, or stack traces in tool output.
- Runs on stdio (local) and an authenticated Streamable HTTP transport (hosted), acting as an OAuth 2.1 resource server.
- True multi-tenancy: each request is bound to its own tenant's Jobber credentials, proven by an automated cross-tenant isolation test.
Tools
Read tools:
| Tool | What it does |
| --- | --- |
| search_clients | Find clients by name, email, or phone. |
| get_client_overview | One call: profile, open jobs, recent quotes, outstanding invoices. |
| list_jobs | List jobs, filterable by status and search term. |
| get_job | Full detail for one job, including line items and visits. |
| find_overdue_invoices | Past-due invoices with client, balance, and days overdue, plus total outstanding. |
| get_schedule | Visits in a date range, optionally by team member. |
Write tools (all preview by default, mutate only with dryRun: false and confirm: true):
| Tool | What it does |
| --- | --- |
| create_quote | Create a quote for a client with line items (auto-resolves the client's property). |
| schedule_job | Create a scheduled visit on a job, optionally assigned to team members. |
| send_invoice | Mark a draft invoice as sent. |
Security
This is the core of the project. Every item below is implemented in the code.
Authentication and token lifecycle
- OAuth 2.0 authorization-code flow against Jobber. Client secret and tokens live only in
.envand gitignored token files, never in source. - Access tokens (60-minute lifetime) are minted on demand from a refresh token and refreshed about 5 minutes before expiry.
- Refreshes are single-flight: concurrent tool calls share one refresh, so a rotating refresh token is never redeemed twice at once.
- The refresh token is persisted to a local,
0600-permission file and updated on every rotation, so the credential chain sustains itself across restarts. - The server loads
.envand overrides inherited environment variables, so a stale value exported into the launching shell cannot silently shadow your configuration.
Multi-tenant design (hosted HTTP transport)
- Each request's tenant comes only from its validated token, never from tool input.
- Each tenant gets its own Jobber client and token-store file; one tenant's credentials are never used for another.
- A session is bound to its tenant at creation and re-checked on every request, so a valid token for one tenant cannot drive another tenant's session. An automated test proves tenant B cannot reach tenant A's session.
Hosted transport authorization
- The server is an OAuth 2.1 resource server. Every HTTP request must carry a bearer token, validated for signature, issuer, expiry, and audience (RFC 8707): a token not issued for this server is rejected.
- Missing or invalid tokens receive a 401 with a
WWW-Authenticateheader pointing at the Protected Resource Metadata (RFC 9728), served at/.well-known/oauth-protected-resource. - The caller's token authenticates them to this server only and is never passed through to Jobber; Jobber credentials are a separate per-tenant secret the server holds as an OAuth client to Jobber.
- The
Originheader is validated (anti DNS-rebinding) and the server binds to localhost by default.
Input and output safety
- Every tool input is validated with
zod(types, ranges, enums, sensible defaults). - Every Jobber response is parsed with
zodbefore use. Unexpected shapes fail safe with a clear message rather than propagating bad data. - Outputs are summarized. Internal cost/margin fields and unnecessary PII are deliberately not returned.
Least-privilege scopes
| Scope | Access | Why | | --- | --- | --- | | Clients | Read | Look up and summarize clients. | | Jobs | Read | List and detail jobs. | | Users | Read | Resolve team members for scheduling. | | Quotes | Read + Write | Read summaries; create quotes. | | Invoices | Read + Write | Find overdue invoices; mark as sent. | | Scheduled Items | Read + Write | Read the schedule; create visits. |
Deliberately not requested: Jobber Payments, Requests, Expenses, Tax Rates, Timesheets, Vehicles, Custom Field Configurations, Marketing. Notably, invoices are marked as sent without requesting payment-data access.
Injection and error safety
- All GraphQL calls use parameterized variables. User input is never concatenated into a query string.
- Errors returned to the client are actionable but safe: no stack traces, no secret values, no internal paths. Diagnostic detail is logged to stderr only, never to stdout (which carries the JSON-RPC protocol).
Observability and rate limits
- Every Jobber API call is audit-logged to stderr (operation name, outcome, duration). No arguments, results, tokens, or PII are logged.
- Both of Jobber's rate limiters (HTTP 429 and the cost-based query throttle) are detected and returned to the caller as a clear, retryable message, while every other failure stays a safe generic message with no internal details.
Destructive and financial guards
- Every write tool previews by default (
dryRun: true) and performs no mutation unless called withdryRun: falseandconfirm: true. - Mutations surface Jobber's
userErrorsinstead of reporting a false success.
Requirements
- Node.js 22 or newer (the server uses native
.envparsing andfetch). - A Jobber developer account, an app registered in the Jobber Developer Center, and a developer test account.
Setup
- Install dependencies and build:
npm install npm run build - Register an app in the Jobber Developer Center. Set the redirect URI to
http://localhost:3000/oauth/callbackand select the scopes in the table above. Scopes cannot be changed after creation. - Copy the env template and fill it in:
Setcp .env.example .envJOBBER_CLIENT_ID,JOBBER_CLIENT_SECRET, andJOBBER_API_VERSION(for example2025-04-16). - Obtain an initial refresh token. Open the authorize URL in a browser (with your client id):
Approve access, copy thehttps://api.getjobber.com/api/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3000/oauth/callback&state=setupcodefrom the redirect URL, and exchange it:
Put the returnedcurl -X POST https://api.getjobber.com/api/oauth/token \ -d grant_type=authorization_code \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d redirect_uri=http://localhost:3000/oauth/callback \ -d code=THE_CODErefresh_tokenin.envasJOBBER_DEV_REFRESH_TOKEN(quoted).
The server mints and refreshes access tokens from there. If refresh-token rotation is enabled on your app, the server persists each new token to .jobber-token.json; keep the server as the only process that redeems the token.
Running the hosted HTTP transport
The HTTP transport adds authenticated, multi-tenant access and needs three more env vars:
MCP_ISSUER=https://dev-issuer.jobber-mcp.local # token issuer this server trusts
MCP_RESOURCE_URL=http://127.0.0.1:3000/mcp # this server's canonical URL (token audience)
MCP_DEV_JWT_SECRET="..." # HS256 secret for dev tokens; openssl rand -hex 32Map each tenant to its Jobber refresh token in a gitignored .tenants.json:
{ "tenant-acme": { "refreshToken": "..." } }The tenant-dev tenant falls back to JOBBER_DEV_REFRESH_TOKEN, so local development needs no tenants file.
Start the server and mint a dev token:
npm run build && npm run start:http
TOKEN=$(npx tsx scripts/mint-dev-token.ts tenant-dev)It serves a single /mcp endpoint plus public Protected Resource Metadata at /.well-known/oauth-protected-resource. Point an MCP client at http://127.0.0.1:3000/mcp in Streamable HTTP mode, with that bearer token.
For production, swap the dev HS256 verifier in src/auth/token-verifier.ts to validate against a real OAuth 2.1 authorization server's JWKS. The wiring does not change.
Connecting to an MCP client
Over stdio, with the MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.jsWith a desktop MCP client, add a server whose command is node with the argument pointing at dist/index.js, providing the environment variables from your .env. Run it from the project directory so the server can find .env and its token file.
Over HTTP: start the server (npm run start:http), then run the Inspector with no command (npx @modelcontextprotocol/inspector), choose Streamable HTTP, use http://127.0.0.1:3000/mcp, and set the bearer token.
Development
npm run dev # stdio server with tsx (watch)
npm run start:http # hosted HTTP transport (built)
npm run dev:http # HTTP transport with tsx watch
npm run mint-token # mint a dev bearer token (arg = tenant id)
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm test # vitest
npm run build # compile to dist/TypeScript is strict, the project is ESM (NodeNext), and tests live next to the code in __tests__/.
Architecture
src/index.tspicks the stdio transport and connects.src/server.tsis the transport-agnostic composition root that registers tools.src/http.tsis the hosted entrypoint:createApp(deps)builds the Express app (Streamable HTTP transport, resource-server auth, per-tenant binding);main()wires the real dependencies.src/jobber/isolates all API access: the GraphQL client, the OAuth refresh, and the rotation-safe token store.src/auth/holds the token verifier;src/tenancy/resolves per-tenant credentials.src/tools/holds one file per tool.src/config/handles env loading and validation.
Limitations and roadmap
- The hosted transport ships with a development (HS256) token issuer. A production deployment points at a real OAuth 2.1 authorization server by swapping the verifier to JWKS validation.
send_invoicesets an invoice's status to sent via Jobber's API. It does not itself email the client (Jobber exposes no send-email mutation).- Automatic retry with backoff on rate limits (beyond detecting and surfacing them) is a future enhancement.
- CI audits production dependencies (
npm audit --omit=dev). A DoS advisory exists in a deep dev-only transitive (brace-expansion, via ESLint); clearing it fully requires ESLint 10, which the currenttypescript-eslintdoes not yet support. It never ships in the package.
License
MIT
