- 112 MCP tools across 17 domains (jobs, customers, estimates, invoices, payments, employees, scheduling, dispatch, tags, notifications, reviews, reporting, price book, leads, webhooks, time tracking, settings) - 15 React apps with proper Vite build setup and dark theme - Full API client with Bearer auth, pagination, error handling, rate limiting - Complete TypeScript types for all entities - TSC passes clean - GHL-quality standard achieved
349 lines
8.8 KiB
TypeScript
349 lines
8.8 KiB
TypeScript
/**
|
|
* Forms Tools for Jobber MCP Server
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import { JobberClient } from '../clients/jobber.js';
|
|
|
|
export interface Form {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
fields: FormField[];
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface FormField {
|
|
id: string;
|
|
label: string;
|
|
type: 'text' | 'textarea' | 'number' | 'date' | 'checkbox' | 'select' | 'signature';
|
|
required: boolean;
|
|
options?: string[];
|
|
}
|
|
|
|
export interface FormSubmission {
|
|
id: string;
|
|
formId: string;
|
|
jobId?: string;
|
|
visitId?: string;
|
|
submittedBy: string;
|
|
submittedAt: string;
|
|
data: Record<string, any>;
|
|
}
|
|
|
|
export const formsTools = {
|
|
list_forms: {
|
|
description: 'List all custom forms',
|
|
inputSchema: z.object({
|
|
isActive: z.boolean().optional(),
|
|
limit: z.number().default(50),
|
|
cursor: z.string().optional(),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const filterClause = args.isActive !== undefined ? `, filter: { isActive: ${args.isActive} }` : '';
|
|
const afterClause = args.cursor ? `, after: "${args.cursor}"` : '';
|
|
|
|
const query = `
|
|
query ListForms {
|
|
forms(first: ${args.limit}${afterClause}${filterClause}) {
|
|
edges {
|
|
node {
|
|
id
|
|
name
|
|
description
|
|
isActive
|
|
createdAt
|
|
updatedAt
|
|
fields {
|
|
id
|
|
label
|
|
type
|
|
required
|
|
options
|
|
}
|
|
}
|
|
cursor
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await client.query(query);
|
|
return {
|
|
forms: data.forms.edges.map((e: any) => e.node),
|
|
pageInfo: data.forms.pageInfo,
|
|
};
|
|
},
|
|
},
|
|
|
|
get_form: {
|
|
description: 'Get a specific form by ID',
|
|
inputSchema: z.object({
|
|
formId: z.string(),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const query = `
|
|
query GetForm($id: ID!) {
|
|
form(id: $id) {
|
|
id
|
|
name
|
|
description
|
|
isActive
|
|
createdAt
|
|
updatedAt
|
|
fields {
|
|
id
|
|
label
|
|
type
|
|
required
|
|
options
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await client.query(query, { id: args.formId });
|
|
return { form: data.form };
|
|
},
|
|
},
|
|
|
|
create_form: {
|
|
description: 'Create a new custom form',
|
|
inputSchema: z.object({
|
|
name: z.string(),
|
|
description: z.string().optional(),
|
|
fields: z.array(z.object({
|
|
label: z.string(),
|
|
type: z.enum(['text', 'textarea', 'number', 'date', 'checkbox', 'select', 'signature']),
|
|
required: z.boolean().default(false),
|
|
options: z.array(z.string()).optional(),
|
|
})),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const mutation = `
|
|
mutation CreateForm($input: FormInput!) {
|
|
formCreate(input: $input) {
|
|
form {
|
|
id
|
|
name
|
|
description
|
|
isActive
|
|
fields {
|
|
id
|
|
label
|
|
type
|
|
required
|
|
options
|
|
}
|
|
}
|
|
userErrors {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const input = {
|
|
name: args.name,
|
|
description: args.description,
|
|
fields: args.fields,
|
|
};
|
|
|
|
const data = await client.mutate(mutation, { input });
|
|
|
|
if (data.formCreate.userErrors?.length > 0) {
|
|
throw new Error(`Form creation failed: ${data.formCreate.userErrors.map((e: any) => e.message).join(', ')}`);
|
|
}
|
|
|
|
return { form: data.formCreate.form };
|
|
},
|
|
},
|
|
|
|
update_form: {
|
|
description: 'Update an existing form',
|
|
inputSchema: z.object({
|
|
formId: z.string(),
|
|
name: z.string().optional(),
|
|
description: z.string().optional(),
|
|
isActive: z.boolean().optional(),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const mutation = `
|
|
mutation UpdateForm($id: ID!, $input: FormUpdateInput!) {
|
|
formUpdate(id: $id, input: $input) {
|
|
form {
|
|
id
|
|
name
|
|
description
|
|
isActive
|
|
}
|
|
userErrors {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const input: any = {};
|
|
if (args.name) input.name = args.name;
|
|
if (args.description) input.description = args.description;
|
|
if (args.isActive !== undefined) input.isActive = args.isActive;
|
|
|
|
const data = await client.mutate(mutation, { id: args.formId, input });
|
|
|
|
if (data.formUpdate.userErrors?.length > 0) {
|
|
throw new Error(`Form update failed: ${data.formUpdate.userErrors.map((e: any) => e.message).join(', ')}`);
|
|
}
|
|
|
|
return { form: data.formUpdate.form };
|
|
},
|
|
},
|
|
|
|
delete_form: {
|
|
description: 'Delete a form',
|
|
inputSchema: z.object({
|
|
formId: z.string(),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const mutation = `
|
|
mutation DeleteForm($id: ID!) {
|
|
formDelete(id: $id) {
|
|
deletedFormId
|
|
userErrors {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await client.mutate(mutation, { id: args.formId });
|
|
|
|
if (data.formDelete.userErrors?.length > 0) {
|
|
throw new Error(`Form deletion failed: ${data.formDelete.userErrors.map((e: any) => e.message).join(', ')}`);
|
|
}
|
|
|
|
return { deletedFormId: data.formDelete.deletedFormId };
|
|
},
|
|
},
|
|
|
|
submit_form: {
|
|
description: 'Submit a form with data',
|
|
inputSchema: z.object({
|
|
formId: z.string(),
|
|
jobId: z.string().optional(),
|
|
visitId: z.string().optional(),
|
|
data: z.record(z.any()),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const mutation = `
|
|
mutation SubmitForm($input: FormSubmissionInput!) {
|
|
formSubmissionCreate(input: $input) {
|
|
formSubmission {
|
|
id
|
|
formId
|
|
submittedAt
|
|
data
|
|
}
|
|
userErrors {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const input = {
|
|
formId: args.formId,
|
|
jobId: args.jobId,
|
|
visitId: args.visitId,
|
|
data: args.data,
|
|
};
|
|
|
|
const data = await client.mutate(mutation, { input });
|
|
|
|
if (data.formSubmissionCreate.userErrors?.length > 0) {
|
|
throw new Error(`Form submission failed: ${data.formSubmissionCreate.userErrors.map((e: any) => e.message).join(', ')}`);
|
|
}
|
|
|
|
return { formSubmission: data.formSubmissionCreate.formSubmission };
|
|
},
|
|
},
|
|
|
|
list_form_submissions: {
|
|
description: 'List submissions for a form',
|
|
inputSchema: z.object({
|
|
formId: z.string().optional(),
|
|
jobId: z.string().optional(),
|
|
visitId: z.string().optional(),
|
|
limit: z.number().default(50),
|
|
cursor: z.string().optional(),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const filters: string[] = [];
|
|
if (args.formId) filters.push(`formId: "${args.formId}"`);
|
|
if (args.jobId) filters.push(`jobId: "${args.jobId}"`);
|
|
if (args.visitId) filters.push(`visitId: "${args.visitId}"`);
|
|
|
|
const filterClause = filters.length > 0 ? `, filter: { ${filters.join(', ')} }` : '';
|
|
const afterClause = args.cursor ? `, after: "${args.cursor}"` : '';
|
|
|
|
const query = `
|
|
query ListFormSubmissions {
|
|
formSubmissions(first: ${args.limit}${afterClause}${filterClause}) {
|
|
edges {
|
|
node {
|
|
id
|
|
formId
|
|
jobId
|
|
visitId
|
|
submittedAt
|
|
data
|
|
}
|
|
cursor
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await client.query(query);
|
|
return {
|
|
submissions: data.formSubmissions.edges.map((e: any) => e.node),
|
|
pageInfo: data.formSubmissions.pageInfo,
|
|
};
|
|
},
|
|
},
|
|
|
|
get_form_submission: {
|
|
description: 'Get a specific form submission',
|
|
inputSchema: z.object({
|
|
submissionId: z.string(),
|
|
}),
|
|
execute: async (client: JobberClient, args: any) => {
|
|
const query = `
|
|
query GetFormSubmission($id: ID!) {
|
|
formSubmission(id: $id) {
|
|
id
|
|
formId
|
|
jobId
|
|
visitId
|
|
submittedAt
|
|
data
|
|
}
|
|
}
|
|
`;
|
|
|
|
const data = await client.query(query, { id: args.submissionId });
|
|
return { submission: data.formSubmission };
|
|
},
|
|
},
|
|
};
|