jobber-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +16 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/auth/oauth.d.ts +19 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +94 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/graphql/queries.d.ts +32 -0
- package/dist/graphql/queries.d.ts.map +1 -0
- package/dist/graphql/queries.js +447 -0
- package/dist/graphql/queries.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/clients.d.ts +6 -0
- package/dist/tools/clients.d.ts.map +1 -0
- package/dist/tools/clients.js +186 -0
- package/dist/tools/clients.js.map +1 -0
- package/dist/tools/invoices.d.ts +6 -0
- package/dist/tools/invoices.d.ts.map +1 -0
- package/dist/tools/invoices.js +64 -0
- package/dist/tools/invoices.js.map +1 -0
- package/dist/tools/jobs.d.ts +6 -0
- package/dist/tools/jobs.d.ts.map +1 -0
- package/dist/tools/jobs.js +205 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/quotes.d.ts +6 -0
- package/dist/tools/quotes.d.ts.map +1 -0
- package/dist/tools/quotes.js +113 -0
- package/dist/tools/quotes.js.map +1 -0
- package/dist/tools/requests.d.ts +6 -0
- package/dist/tools/requests.d.ts.map +1 -0
- package/dist/tools/requests.js +117 -0
- package/dist/tools/requests.js.map +1 -0
- package/dist/tools/scheduling.d.ts +6 -0
- package/dist/tools/scheduling.d.ts.map +1 -0
- package/dist/tools/scheduling.js +197 -0
- package/dist/tools/scheduling.js.map +1 -0
- package/dist/transport.d.ts +7 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +75 -0
- package/dist/transport.js.map +1 -0
- package/dist/utils/errors.d.ts +42 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +72 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/pagination.d.ts +29 -0
- package/dist/utils/pagination.d.ts.map +1 -0
- package/dist/utils/pagination.js +49 -0
- package/dist/utils/pagination.js.map +1 -0
- package/package.json +55 -0
- package/scripts/extract-tokens.py +46 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jobber GraphQL queries, mutations, and the core request function.
|
|
3
|
+
*
|
|
4
|
+
* All GraphQL operations are defined here as tagged template strings.
|
|
5
|
+
* The jobberRequest() function handles auth, retries, and rate limiting.
|
|
6
|
+
*/
|
|
7
|
+
import { getTokens, isTokenExpiringSoon, refreshAccessToken, } from "../auth/oauth.js";
|
|
8
|
+
import { JobberAPIError, extractErrors } from "../utils/errors.js";
|
|
9
|
+
const API_URL = "https://api.getjobber.com/api/graphql";
|
|
10
|
+
const API_VERSION = "2025-04-16";
|
|
11
|
+
// Simple rate limiter: track request timestamps
|
|
12
|
+
const requestTimestamps = [];
|
|
13
|
+
const RATE_LIMIT = 60;
|
|
14
|
+
const RATE_WINDOW_MS = 60_000;
|
|
15
|
+
async function waitForRateLimit() {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
// Purge old timestamps
|
|
18
|
+
while (requestTimestamps.length > 0 && requestTimestamps[0] < now - RATE_WINDOW_MS) {
|
|
19
|
+
requestTimestamps.shift();
|
|
20
|
+
}
|
|
21
|
+
if (requestTimestamps.length >= RATE_LIMIT) {
|
|
22
|
+
const oldestInWindow = requestTimestamps[0];
|
|
23
|
+
const waitMs = oldestInWindow + RATE_WINDOW_MS - now + 100;
|
|
24
|
+
console.error(`[jobber-mcp] Rate limit approaching, waiting ${waitMs}ms`);
|
|
25
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
26
|
+
}
|
|
27
|
+
requestTimestamps.push(Date.now());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Execute a GraphQL request against the Jobber API.
|
|
31
|
+
* Handles token refresh, rate limiting, and retries.
|
|
32
|
+
*/
|
|
33
|
+
export async function jobberRequest(query, variables) {
|
|
34
|
+
// Refresh token if expiring soon
|
|
35
|
+
if (isTokenExpiringSoon()) {
|
|
36
|
+
await refreshAccessToken();
|
|
37
|
+
}
|
|
38
|
+
await waitForRateLimit();
|
|
39
|
+
const tokens = getTokens();
|
|
40
|
+
const headers = {
|
|
41
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"X-JOBBER-GRAPHQL-VERSION": API_VERSION,
|
|
44
|
+
};
|
|
45
|
+
const body = JSON.stringify({
|
|
46
|
+
query,
|
|
47
|
+
...(variables ? { variables } : {}),
|
|
48
|
+
});
|
|
49
|
+
let lastError = null;
|
|
50
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
51
|
+
const resp = await fetch(API_URL, { method: "POST", headers, body });
|
|
52
|
+
if (resp.status === 429) {
|
|
53
|
+
const wait = (2 ** attempt) * 2000;
|
|
54
|
+
console.error(`[jobber-mcp] Rate limited (429), retrying in ${wait}ms (attempt ${attempt + 1})`);
|
|
55
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (resp.status === 401 && attempt === 0) {
|
|
59
|
+
console.error("[jobber-mcp] Got 401, attempting token refresh...");
|
|
60
|
+
await refreshAccessToken();
|
|
61
|
+
const newTokens = getTokens();
|
|
62
|
+
headers.Authorization = `Bearer ${newTokens.accessToken}`;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!resp.ok) {
|
|
66
|
+
const text = await resp.text();
|
|
67
|
+
throw new JobberAPIError(`Jobber API returned ${resp.status}: ${text}`, {
|
|
68
|
+
statusCode: resp.status,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const data = (await resp.json());
|
|
72
|
+
const errorMsg = extractErrors(data);
|
|
73
|
+
if (errorMsg) {
|
|
74
|
+
throw new JobberAPIError(errorMsg, {
|
|
75
|
+
graphqlErrors: data.errors,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return (data.data ?? {});
|
|
79
|
+
}
|
|
80
|
+
throw lastError ?? new JobberAPIError("Request failed after 3 retries");
|
|
81
|
+
}
|
|
82
|
+
// ─── Client Queries ──────────────────────────────────────────────────
|
|
83
|
+
export const SEARCH_CLIENTS = `
|
|
84
|
+
query SearchClients($searchTerm: String!, $first: Int!, $after: String) {
|
|
85
|
+
clients(searchTerm: $searchTerm, first: $first, after: $after) {
|
|
86
|
+
nodes {
|
|
87
|
+
id
|
|
88
|
+
firstName
|
|
89
|
+
lastName
|
|
90
|
+
name
|
|
91
|
+
isCompany
|
|
92
|
+
companyName
|
|
93
|
+
phones { number description primary }
|
|
94
|
+
emails { address description primary }
|
|
95
|
+
billingAddress { street1 street2 city province postalCode country }
|
|
96
|
+
tags { nodes { label } }
|
|
97
|
+
createdAt
|
|
98
|
+
}
|
|
99
|
+
totalCount
|
|
100
|
+
pageInfo { hasNextPage endCursor }
|
|
101
|
+
}
|
|
102
|
+
}`;
|
|
103
|
+
export const GET_CLIENT = `
|
|
104
|
+
query GetClient($id: EncodedId!) {
|
|
105
|
+
client(id: $id) {
|
|
106
|
+
id
|
|
107
|
+
firstName
|
|
108
|
+
lastName
|
|
109
|
+
name
|
|
110
|
+
isCompany
|
|
111
|
+
companyName
|
|
112
|
+
phones { number description primary }
|
|
113
|
+
emails { address description primary }
|
|
114
|
+
billingAddress { street1 street2 city province postalCode country }
|
|
115
|
+
tags { nodes { label } }
|
|
116
|
+
createdAt
|
|
117
|
+
clientProperties(first: 10) {
|
|
118
|
+
nodes {
|
|
119
|
+
id
|
|
120
|
+
address { street1 street2 city province postalCode country }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
jobs(first: 10, orderBy: { key: CREATED_AT, direction: DESC }) {
|
|
124
|
+
nodes {
|
|
125
|
+
id
|
|
126
|
+
title
|
|
127
|
+
jobStatus
|
|
128
|
+
createdAt
|
|
129
|
+
total
|
|
130
|
+
}
|
|
131
|
+
totalCount
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}`;
|
|
135
|
+
export const CREATE_CLIENT = `
|
|
136
|
+
mutation CreateClient($input: ClientCreateInput!) {
|
|
137
|
+
clientCreate(input: $input) {
|
|
138
|
+
client {
|
|
139
|
+
id
|
|
140
|
+
firstName
|
|
141
|
+
lastName
|
|
142
|
+
name
|
|
143
|
+
jobberWebUri
|
|
144
|
+
}
|
|
145
|
+
userErrors { message path }
|
|
146
|
+
}
|
|
147
|
+
}`;
|
|
148
|
+
export const UPDATE_CLIENT = `
|
|
149
|
+
mutation UpdateClient($clientId: EncodedId!, $input: ClientUpdateInput!) {
|
|
150
|
+
clientUpdate(clientId: $clientId, input: $input) {
|
|
151
|
+
client {
|
|
152
|
+
id
|
|
153
|
+
firstName
|
|
154
|
+
lastName
|
|
155
|
+
name
|
|
156
|
+
phones { number description primary }
|
|
157
|
+
emails { address description primary }
|
|
158
|
+
}
|
|
159
|
+
userErrors { message path }
|
|
160
|
+
}
|
|
161
|
+
}`;
|
|
162
|
+
// ─── Request Queries ─────────────────────────────────────────────────
|
|
163
|
+
export const CREATE_REQUEST = `
|
|
164
|
+
mutation CreateRequest($input: RequestCreateInput!) {
|
|
165
|
+
requestCreate(input: $input) {
|
|
166
|
+
request {
|
|
167
|
+
id
|
|
168
|
+
title
|
|
169
|
+
requestStatus
|
|
170
|
+
jobberWebUri
|
|
171
|
+
createdAt
|
|
172
|
+
}
|
|
173
|
+
userErrors { message path }
|
|
174
|
+
}
|
|
175
|
+
}`;
|
|
176
|
+
export const LIST_REQUESTS = `
|
|
177
|
+
query ListRequests($first: Int!, $after: String) {
|
|
178
|
+
requests(first: $first, after: $after) {
|
|
179
|
+
nodes {
|
|
180
|
+
id
|
|
181
|
+
title
|
|
182
|
+
requestStatus
|
|
183
|
+
createdAt
|
|
184
|
+
client { id name }
|
|
185
|
+
property { address { street1 city province } }
|
|
186
|
+
jobberWebUri
|
|
187
|
+
}
|
|
188
|
+
totalCount
|
|
189
|
+
pageInfo { hasNextPage endCursor }
|
|
190
|
+
}
|
|
191
|
+
}`;
|
|
192
|
+
export const GET_REQUEST = `
|
|
193
|
+
query GetRequest($id: EncodedId!) {
|
|
194
|
+
request(id: $id) {
|
|
195
|
+
id
|
|
196
|
+
title
|
|
197
|
+
requestStatus
|
|
198
|
+
createdAt
|
|
199
|
+
client { id name phones { number } emails { address } }
|
|
200
|
+
property { address { street1 street2 city province postalCode } }
|
|
201
|
+
jobberWebUri
|
|
202
|
+
}
|
|
203
|
+
}`;
|
|
204
|
+
export const CREATE_REQUEST_NOTE = `
|
|
205
|
+
mutation CreateRequestNote($requestId: EncodedId!, $message: String!) {
|
|
206
|
+
requestNoteCreate(requestId: $requestId, message: $message) {
|
|
207
|
+
note { id }
|
|
208
|
+
userErrors { message path }
|
|
209
|
+
}
|
|
210
|
+
}`;
|
|
211
|
+
// ─── Job Queries ─────────────────────────────────────────────────────
|
|
212
|
+
export const LIST_JOBS = `
|
|
213
|
+
query ListJobs($first: Int!, $after: String, $filter: JobFilterAttributes) {
|
|
214
|
+
jobs(first: $first, after: $after, filter: $filter) {
|
|
215
|
+
nodes {
|
|
216
|
+
id
|
|
217
|
+
title
|
|
218
|
+
jobNumber
|
|
219
|
+
jobStatus
|
|
220
|
+
startAt
|
|
221
|
+
endAt
|
|
222
|
+
createdAt
|
|
223
|
+
total
|
|
224
|
+
client { id name }
|
|
225
|
+
property { address { street1 city province postalCode } }
|
|
226
|
+
jobberWebUri
|
|
227
|
+
}
|
|
228
|
+
totalCount
|
|
229
|
+
pageInfo { hasNextPage endCursor }
|
|
230
|
+
}
|
|
231
|
+
}`;
|
|
232
|
+
export const GET_JOB = `
|
|
233
|
+
query GetJob($id: EncodedId!) {
|
|
234
|
+
job(id: $id) {
|
|
235
|
+
id
|
|
236
|
+
title
|
|
237
|
+
jobNumber
|
|
238
|
+
jobStatus
|
|
239
|
+
startAt
|
|
240
|
+
endAt
|
|
241
|
+
createdAt
|
|
242
|
+
total
|
|
243
|
+
instructions
|
|
244
|
+
client {
|
|
245
|
+
id
|
|
246
|
+
name
|
|
247
|
+
firstName
|
|
248
|
+
lastName
|
|
249
|
+
phones { number description }
|
|
250
|
+
emails { address description }
|
|
251
|
+
}
|
|
252
|
+
property {
|
|
253
|
+
address { street1 street2 city province postalCode }
|
|
254
|
+
}
|
|
255
|
+
lineItems {
|
|
256
|
+
nodes {
|
|
257
|
+
name
|
|
258
|
+
description
|
|
259
|
+
quantity
|
|
260
|
+
unitPrice
|
|
261
|
+
totalPrice
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
visits(first: 20) {
|
|
265
|
+
nodes {
|
|
266
|
+
id
|
|
267
|
+
title
|
|
268
|
+
startAt
|
|
269
|
+
endAt
|
|
270
|
+
status
|
|
271
|
+
assignedUsers { nodes { id name { full } } }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
jobberWebUri
|
|
275
|
+
}
|
|
276
|
+
}`;
|
|
277
|
+
export const CREATE_JOB = `
|
|
278
|
+
mutation CreateJob($input: JobCreateInput!) {
|
|
279
|
+
jobCreate(input: $input) {
|
|
280
|
+
job {
|
|
281
|
+
id
|
|
282
|
+
title
|
|
283
|
+
jobNumber
|
|
284
|
+
jobStatus
|
|
285
|
+
jobberWebUri
|
|
286
|
+
}
|
|
287
|
+
userErrors { message path }
|
|
288
|
+
}
|
|
289
|
+
}`;
|
|
290
|
+
export const CREATE_JOB_NOTE = `
|
|
291
|
+
mutation CreateJobNote($jobId: ID!, $message: String!) {
|
|
292
|
+
jobNoteCreate(jobId: $jobId, message: $message) {
|
|
293
|
+
note { id }
|
|
294
|
+
userErrors { message path }
|
|
295
|
+
}
|
|
296
|
+
}`;
|
|
297
|
+
// ─── Scheduling / Visit Queries ──────────────────────────────────────
|
|
298
|
+
export const GET_SCHEDULE = `
|
|
299
|
+
query GetSchedule($startAt: ISO8601Date!, $endAt: ISO8601Date!, $first: Int!, $after: String) {
|
|
300
|
+
jobs(
|
|
301
|
+
filter: {
|
|
302
|
+
startAt: { between: { start: $startAt, end: $endAt } }
|
|
303
|
+
status: [ACTIVE, IN_PROGRESS, TODAY, UPCOMING]
|
|
304
|
+
}
|
|
305
|
+
first: $first
|
|
306
|
+
after: $after
|
|
307
|
+
) {
|
|
308
|
+
nodes {
|
|
309
|
+
id
|
|
310
|
+
title
|
|
311
|
+
jobStatus
|
|
312
|
+
startAt
|
|
313
|
+
endAt
|
|
314
|
+
client { id name }
|
|
315
|
+
property { address { street1 city province } }
|
|
316
|
+
visits(first: 20) {
|
|
317
|
+
nodes {
|
|
318
|
+
id
|
|
319
|
+
startAt
|
|
320
|
+
endAt
|
|
321
|
+
status
|
|
322
|
+
assignedUsers { nodes { id name { full } } }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
jobberWebUri
|
|
326
|
+
}
|
|
327
|
+
totalCount
|
|
328
|
+
pageInfo { hasNextPage endCursor }
|
|
329
|
+
}
|
|
330
|
+
}`;
|
|
331
|
+
export const CREATE_VISIT = `
|
|
332
|
+
mutation CreateVisit($input: VisitCreateInput!) {
|
|
333
|
+
visitCreate(input: $input) {
|
|
334
|
+
visit {
|
|
335
|
+
id
|
|
336
|
+
startAt
|
|
337
|
+
endAt
|
|
338
|
+
}
|
|
339
|
+
userErrors { message path }
|
|
340
|
+
}
|
|
341
|
+
}`;
|
|
342
|
+
// ─── Quote Queries ───────────────────────────────────────────────────
|
|
343
|
+
export const CREATE_QUOTE = `
|
|
344
|
+
mutation CreateQuote($attributes: QuoteCreateAttributes!) {
|
|
345
|
+
quoteCreate(attributes: $attributes) {
|
|
346
|
+
quote {
|
|
347
|
+
id
|
|
348
|
+
quoteNumber
|
|
349
|
+
quoteStatus
|
|
350
|
+
total
|
|
351
|
+
jobberWebUri
|
|
352
|
+
}
|
|
353
|
+
userErrors { message path }
|
|
354
|
+
}
|
|
355
|
+
}`;
|
|
356
|
+
export const LIST_QUOTES = `
|
|
357
|
+
query ListQuotes($first: Int!, $after: String) {
|
|
358
|
+
quotes(first: $first, after: $after) {
|
|
359
|
+
nodes {
|
|
360
|
+
id
|
|
361
|
+
quoteNumber
|
|
362
|
+
quoteStatus
|
|
363
|
+
total
|
|
364
|
+
createdAt
|
|
365
|
+
client { id name }
|
|
366
|
+
jobberWebUri
|
|
367
|
+
}
|
|
368
|
+
totalCount
|
|
369
|
+
pageInfo { hasNextPage endCursor }
|
|
370
|
+
}
|
|
371
|
+
}`;
|
|
372
|
+
// ─── Invoice Queries ─────────────────────────────────────────────────
|
|
373
|
+
export const LIST_INVOICES = `
|
|
374
|
+
query ListInvoices($first: Int!, $after: String) {
|
|
375
|
+
invoices(first: $first, after: $after) {
|
|
376
|
+
nodes {
|
|
377
|
+
id
|
|
378
|
+
invoiceNumber
|
|
379
|
+
invoiceStatus
|
|
380
|
+
total
|
|
381
|
+
amountDue
|
|
382
|
+
issuedDate
|
|
383
|
+
dueDate
|
|
384
|
+
createdAt
|
|
385
|
+
client { id name }
|
|
386
|
+
jobberWebUri
|
|
387
|
+
}
|
|
388
|
+
totalCount
|
|
389
|
+
pageInfo { hasNextPage endCursor }
|
|
390
|
+
}
|
|
391
|
+
}`;
|
|
392
|
+
export const GET_INVOICE = `
|
|
393
|
+
query GetInvoice($id: EncodedId!) {
|
|
394
|
+
invoice(id: $id) {
|
|
395
|
+
id
|
|
396
|
+
invoiceNumber
|
|
397
|
+
invoiceStatus
|
|
398
|
+
total
|
|
399
|
+
amountDue
|
|
400
|
+
issuedDate
|
|
401
|
+
dueDate
|
|
402
|
+
createdAt
|
|
403
|
+
subject
|
|
404
|
+
message
|
|
405
|
+
client {
|
|
406
|
+
id
|
|
407
|
+
name
|
|
408
|
+
firstName
|
|
409
|
+
lastName
|
|
410
|
+
phones { number }
|
|
411
|
+
emails { address }
|
|
412
|
+
}
|
|
413
|
+
lineItems {
|
|
414
|
+
nodes {
|
|
415
|
+
name
|
|
416
|
+
description
|
|
417
|
+
quantity
|
|
418
|
+
unitPrice
|
|
419
|
+
totalPrice
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
jobberWebUri
|
|
423
|
+
}
|
|
424
|
+
}`;
|
|
425
|
+
// ─── Account ─────────────────────────────────────────────────────────
|
|
426
|
+
export const GET_ACCOUNT = `
|
|
427
|
+
query GetAccount {
|
|
428
|
+
account {
|
|
429
|
+
id
|
|
430
|
+
name
|
|
431
|
+
phone
|
|
432
|
+
industry
|
|
433
|
+
}
|
|
434
|
+
}`;
|
|
435
|
+
// ─── Users (for team member lookups in scheduling) ───────────────────
|
|
436
|
+
export const LIST_USERS = `
|
|
437
|
+
query ListUsers($first: Int!) {
|
|
438
|
+
users(first: $first) {
|
|
439
|
+
nodes {
|
|
440
|
+
id
|
|
441
|
+
name { first last full }
|
|
442
|
+
email { raw }
|
|
443
|
+
role
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}`;
|
|
447
|
+
//# sourceMappingURL=queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/graphql/queries.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Jobber MCP Server — provides Claude (or any MCP client) with tools
|
|
4
|
+
* to interact with a Jobber account via the GraphQL API.
|
|
5
|
+
*
|
|
6
|
+
* Supports two transport modes:
|
|
7
|
+
* - stdio (default): for Claude Code / local dev
|
|
8
|
+
* - streamable-http: for remote/hosted deployments
|
|
9
|
+
*
|
|
10
|
+
* Set TRANSPORT=http to use HTTP mode, or leave unset for stdio.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;GASG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Jobber MCP Server — provides Claude (or any MCP client) with tools
|
|
4
|
+
* to interact with a Jobber account via the GraphQL API.
|
|
5
|
+
*
|
|
6
|
+
* Supports two transport modes:
|
|
7
|
+
* - stdio (default): for Claude Code / local dev
|
|
8
|
+
* - streamable-http: for remote/hosted deployments
|
|
9
|
+
*
|
|
10
|
+
* Set TRANSPORT=http to use HTTP mode, or leave unset for stdio.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { resolve, dirname } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { loadTokensFromEnv } from "./auth/oauth.js";
|
|
17
|
+
// Load .env from project root (works for both src/ and dist/)
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const envPath = resolve(__dirname, "..", ".env");
|
|
20
|
+
try {
|
|
21
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
22
|
+
for (const line of envContent.split("\n")) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
25
|
+
continue;
|
|
26
|
+
const eqIdx = trimmed.indexOf("=");
|
|
27
|
+
if (eqIdx === -1)
|
|
28
|
+
continue;
|
|
29
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
30
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
31
|
+
if (!process.env[key])
|
|
32
|
+
process.env[key] = val; // don't override existing env
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// .env is optional — env vars can be passed directly
|
|
37
|
+
}
|
|
38
|
+
import { registerClientTools } from "./tools/clients.js";
|
|
39
|
+
import { registerRequestTools } from "./tools/requests.js";
|
|
40
|
+
import { registerJobTools } from "./tools/jobs.js";
|
|
41
|
+
import { registerSchedulingTools } from "./tools/scheduling.js";
|
|
42
|
+
import { registerQuoteTools } from "./tools/quotes.js";
|
|
43
|
+
import { registerInvoiceTools } from "./tools/invoices.js";
|
|
44
|
+
import { startStdioTransport, startHttpTransport } from "./transport.js";
|
|
45
|
+
async function main() {
|
|
46
|
+
// Validate auth tokens are available
|
|
47
|
+
try {
|
|
48
|
+
loadTokensFromEnv();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error(`[jobber-mcp] ${error.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Create MCP server
|
|
55
|
+
const server = new McpServer({
|
|
56
|
+
name: "jobber",
|
|
57
|
+
version: "1.0.0",
|
|
58
|
+
});
|
|
59
|
+
// Register all tool categories
|
|
60
|
+
registerClientTools(server);
|
|
61
|
+
registerRequestTools(server);
|
|
62
|
+
registerJobTools(server);
|
|
63
|
+
registerSchedulingTools(server);
|
|
64
|
+
registerQuoteTools(server);
|
|
65
|
+
registerInvoiceTools(server);
|
|
66
|
+
// Start the appropriate transport
|
|
67
|
+
const transport = process.env.TRANSPORT ?? "stdio";
|
|
68
|
+
if (transport === "http") {
|
|
69
|
+
await startHttpTransport(server);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await startStdioTransport(server);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
main().catch((error) => {
|
|
76
|
+
console.error("[jobber-mcp] Fatal error:", error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../../src/tools/clients.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAWpE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAkO3D"}
|