From 9c9c60d0eb627ad0a9ce712c1522b6c7c1170622 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Fri, 17 Apr 2026 12:16:23 +0200 Subject: [PATCH] Add webhook subscription MCP tools and N8N operations Expose webhook subscription CRUD and event listing through the MCP API (list, get, create, update, delete subscriptions + list events) and add a new webhook resource to the N8N node with matching operations. Signed-off-by: Sacha Al Himdani --- packages/n8n-node/nodes/Probo/Probo.node.ts | 5 + .../n8n-node/nodes/Probo/actions/index.ts | 2 + .../Probo/actions/webhook/create.operation.ts | 114 ++++++ .../Probo/actions/webhook/delete.operation.ts | 57 +++ .../Probo/actions/webhook/get.operation.ts | 61 ++++ .../Probo/actions/webhook/getAll.operation.ts | 114 ++++++ .../actions/webhook/getEvents.operation.ts | 114 ++++++ .../nodes/Probo/actions/webhook/index.ts | 82 +++++ .../Probo/actions/webhook/update.operation.ts | 107 ++++++ pkg/server/api/mcp/v1/schema.resolvers.go | 126 +++++++ pkg/server/api/mcp/v1/specification.yaml | 330 ++++++++++++++++++ .../api/mcp/v1/types/webhook_subscription.go | 88 +++++ 12 files changed, 1200 insertions(+) create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/create.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/delete.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/get.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/getAll.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/getEvents.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/index.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/webhook/update.operation.ts create mode 100644 pkg/server/api/mcp/v1/types/webhook_subscription.go diff --git a/packages/n8n-node/nodes/Probo/Probo.node.ts b/packages/n8n-node/nodes/Probo/Probo.node.ts index 25225506d..341973349 100644 --- a/packages/n8n-node/nodes/Probo/Probo.node.ts +++ b/packages/n8n-node/nodes/Probo/Probo.node.ts @@ -146,6 +146,11 @@ export class Probo implements INodeType { value: 'vendor', description: 'Manage vendors', }, + { + name: 'Webhook', + value: 'webhook', + description: 'Manage webhook subscriptions', + }, ], default: 'execute', }, diff --git a/packages/n8n-node/nodes/Probo/actions/index.ts b/packages/n8n-node/nodes/Probo/actions/index.ts index 184c075d4..9f206ba47 100644 --- a/packages/n8n-node/nodes/Probo/actions/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/index.ts @@ -27,6 +27,7 @@ import * as user from './user'; import * as risk from './risk'; import * as statementOfApplicability from './statementOfApplicability'; import * as vendor from './vendor'; +import * as webhook from './webhook'; export interface ResourceModule { description: INodeProperties[]; @@ -53,6 +54,7 @@ export const resources: Record = { risk: risk as ResourceModule, statementOfApplicability: statementOfApplicability as ResourceModule, vendor: vendor as ResourceModule, + webhook: webhook as ResourceModule, }; export function getAllResourceOperations(): INodeProperties[] { diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/webhook/create.operation.ts new file mode 100644 index 000000000..99298e8c4 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/create.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Endpoint URL', + name: 'endpointUrl', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['create'], + }, + }, + default: '', + description: 'The HTTPS endpoint URL that receives webhook events', + required: true, + }, + { + displayName: 'Selected Events', + name: 'selectedEvents', + type: 'multiOptions', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['create'], + }, + }, + options: [ + { name: 'Meeting Created', value: 'MEETING_CREATED' }, + { name: 'Meeting Deleted', value: 'MEETING_DELETED' }, + { name: 'Meeting Updated', value: 'MEETING_UPDATED' }, + { name: 'Obligation Created', value: 'OBLIGATION_CREATED' }, + { name: 'Obligation Deleted', value: 'OBLIGATION_DELETED' }, + { name: 'Obligation Updated', value: 'OBLIGATION_UPDATED' }, + { name: 'User Created', value: 'USER_CREATED' }, + { name: 'User Deleted', value: 'USER_DELETED' }, + { name: 'User Updated', value: 'USER_UPDATED' }, + { name: 'Vendor Created', value: 'VENDOR_CREATED' }, + { name: 'Vendor Deleted', value: 'VENDOR_DELETED' }, + { name: 'Vendor Updated', value: 'VENDOR_UPDATED' }, + ], + default: [], + description: 'The event types to subscribe to', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const endpointUrl = this.getNodeParameter('endpointUrl', itemIndex) as string; + const selectedEvents = this.getNodeParameter('selectedEvents', itemIndex) as string[]; + + const query = ` + mutation CreateWebhookSubscription($input: CreateWebhookSubscriptionInput!) { + createWebhookSubscription(input: $input) { + webhookSubscriptionEdge { + node { + id + endpointUrl + signingSecret + selectedEvents + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { + organizationId, + endpointUrl, + selectedEvents, + }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/webhook/delete.operation.ts new file mode 100644 index 000000000..79640e48f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/delete.operation.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Webhook Subscription ID', + name: 'webhookSubscriptionId', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the webhook subscription to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const webhookSubscriptionId = this.getNodeParameter('webhookSubscriptionId', itemIndex) as string; + + const query = ` + mutation DeleteWebhookSubscription($input: DeleteWebhookSubscriptionInput!) { + deleteWebhookSubscription(input: $input) { + deletedWebhookSubscriptionId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { webhookSubscriptionId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/webhook/get.operation.ts new file mode 100644 index 000000000..233b06078 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/get.operation.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Webhook Subscription ID', + name: 'webhookSubscriptionId', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the webhook subscription', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const webhookSubscriptionId = this.getNodeParameter('webhookSubscriptionId', itemIndex) as string; + + const query = ` + query GetWebhookSubscription($webhookSubscriptionId: ID!) { + node(id: $webhookSubscriptionId) { + ... on WebhookSubscription { + id + endpointUrl + selectedEvents + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { webhookSubscriptionId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/webhook/getAll.operation.ts new file mode 100644 index 000000000..732e5d984 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/getAll.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetWebhookSubscriptions($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + webhookSubscriptions(first: $first, after: $after) { + edges { + node { + id + endpointUrl + selectedEvents + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const webhookSubscriptions = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.webhookSubscriptions as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { webhookSubscriptions }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/getEvents.operation.ts b/packages/n8n-node/nodes/Probo/actions/webhook/getEvents.operation.ts new file mode 100644 index 000000000..121b40e6c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/getEvents.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Webhook Subscription ID', + name: 'webhookSubscriptionId', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['getEvents'], + }, + }, + default: '', + description: 'The ID of the webhook subscription', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['getEvents'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['getEvents'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const webhookSubscriptionId = this.getNodeParameter('webhookSubscriptionId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetWebhookEvents($webhookSubscriptionId: ID!, $first: Int, $after: CursorKey) { + node(id: $webhookSubscriptionId) { + ... on WebhookSubscription { + events(first: $first, after: $after) { + edges { + node { + id + webhookSubscriptionId + status + response + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const events = await proboApiRequestAllItems.call( + this, + query, + { webhookSubscriptionId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.events as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { events }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/index.ts b/packages/n8n-node/nodes/Probo/actions/webhook/index.ts new file mode 100644 index 000000000..c7e82ae3d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/index.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; +import * as getEventsOp from './getEvents.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['webhook'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new webhook subscription', + action: 'Create a webhook subscription', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a webhook subscription', + action: 'Delete a webhook subscription', + }, + { + name: 'Get', + value: 'get', + description: 'Get a webhook subscription', + action: 'Get a webhook subscription', + }, + { + name: 'Get Events', + value: 'getEvents', + description: 'Get delivery events for a webhook subscription', + action: 'Get webhook delivery events', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many webhook subscriptions', + action: 'Get many webhook subscriptions', + }, + { + name: 'Update', + value: 'update', + description: 'Update a webhook subscription', + action: 'Update a webhook subscription', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, + ...getEventsOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll, getEventsOp as getEvents }; diff --git a/packages/n8n-node/nodes/Probo/actions/webhook/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/webhook/update.operation.ts new file mode 100644 index 000000000..ec6c34c50 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/webhook/update.operation.ts @@ -0,0 +1,107 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Webhook Subscription ID', + name: 'webhookSubscriptionId', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the webhook subscription to update', + required: true, + }, + { + displayName: 'Endpoint URL', + name: 'endpointUrl', + type: 'string', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['update'], + }, + }, + default: '', + description: 'The HTTPS endpoint URL that receives webhook events', + }, + { + displayName: 'Selected Events', + name: 'selectedEvents', + type: 'multiOptions', + displayOptions: { + show: { + resource: ['webhook'], + operation: ['update'], + }, + }, + options: [ + { name: 'Meeting Created', value: 'MEETING_CREATED' }, + { name: 'Meeting Deleted', value: 'MEETING_DELETED' }, + { name: 'Meeting Updated', value: 'MEETING_UPDATED' }, + { name: 'Obligation Created', value: 'OBLIGATION_CREATED' }, + { name: 'Obligation Deleted', value: 'OBLIGATION_DELETED' }, + { name: 'Obligation Updated', value: 'OBLIGATION_UPDATED' }, + { name: 'User Created', value: 'USER_CREATED' }, + { name: 'User Deleted', value: 'USER_DELETED' }, + { name: 'User Updated', value: 'USER_UPDATED' }, + { name: 'Vendor Created', value: 'VENDOR_CREATED' }, + { name: 'Vendor Deleted', value: 'VENDOR_DELETED' }, + { name: 'Vendor Updated', value: 'VENDOR_UPDATED' }, + ], + default: [], + description: 'The event types to subscribe to (replaces existing selection)', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const webhookSubscriptionId = this.getNodeParameter('webhookSubscriptionId', itemIndex) as string; + const endpointUrl = this.getNodeParameter('endpointUrl', itemIndex, '') as string; + const selectedEvents = this.getNodeParameter('selectedEvents', itemIndex, []) as string[]; + + const query = ` + mutation UpdateWebhookSubscription($input: UpdateWebhookSubscriptionInput!) { + updateWebhookSubscription(input: $input) { + webhookSubscription { + id + endpointUrl + selectedEvents + createdAt + updatedAt + } + } + } + `; + + const input: Record = { webhookSubscriptionId }; + if (endpointUrl) input.endpointUrl = endpointUrl; + if (selectedEvents !== undefined) input.selectedEvents = selectedEvents; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 8401a8255..ad5740a38 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -3922,3 +3922,129 @@ func (r *Resolver) PublishStatementOfApplicabilityTool(ctx context.Context, req DocumentVersionID: documentVersion.ID, }, nil } + +func (r *Resolver) ListWebhookSubscriptionsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListWebhookSubscriptionsInput) (*mcp.CallToolResult, types.ListWebhookSubscriptionsOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionWebhookSubscriptionList) + + prb := r.ProboService(ctx, input.OrganizationID) + + pageOrderBy := page.OrderBy[coredata.WebhookSubscriptionOrderField]{ + Field: coredata.WebhookSubscriptionOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.WebhookSubscriptionOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + page, err := prb.WebhookSubscriptions.ListForOrganizationID(ctx, input.OrganizationID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list webhook subscriptions: %w", err)) + } + + return nil, types.NewListWebhookSubscriptionsOutput(page), nil +} + +func (r *Resolver) GetWebhookSubscriptionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetWebhookSubscriptionInput) (*mcp.CallToolResult, types.GetWebhookSubscriptionOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionWebhookSubscriptionGet) + + prb := r.ProboService(ctx, input.ID) + + subscription, err := prb.WebhookSubscriptions.Get(ctx, input.ID) + if err != nil { + return nil, types.GetWebhookSubscriptionOutput{}, fmt.Errorf("failed to get webhook subscription: %w", err) + } + + return nil, types.GetWebhookSubscriptionOutput{ + WebhookSubscription: types.NewWebhookSubscription(subscription), + }, nil +} + +func (r *Resolver) CreateWebhookSubscriptionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateWebhookSubscriptionInput) (*mcp.CallToolResult, types.CreateWebhookSubscriptionOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionWebhookSubscriptionCreate) + + prb := r.ProboService(ctx, input.OrganizationID) + + subscription, err := prb.WebhookSubscriptions.Create( + ctx, + probo.CreateWebhookSubscriptionRequest{ + OrganizationID: input.OrganizationID, + EndpointURL: input.EndpointURL, + SelectedEvents: input.SelectedEvents, + }, + ) + if err != nil { + return nil, types.CreateWebhookSubscriptionOutput{}, fmt.Errorf("failed to create webhook subscription: %w", err) + } + + return nil, types.CreateWebhookSubscriptionOutput{ + WebhookSubscription: types.NewWebhookSubscription(subscription), + }, nil +} + +func (r *Resolver) UpdateWebhookSubscriptionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateWebhookSubscriptionInput) (*mcp.CallToolResult, types.UpdateWebhookSubscriptionOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionWebhookSubscriptionUpdate) + + prb := r.ProboService(ctx, input.ID) + + subscription, err := prb.WebhookSubscriptions.Update( + ctx, + probo.UpdateWebhookSubscriptionRequest{ + WebhookSubscriptionID: input.ID, + EndpointURL: input.EndpointURL, + SelectedEvents: input.SelectedEvents, + }, + ) + if err != nil { + return nil, types.UpdateWebhookSubscriptionOutput{}, fmt.Errorf("failed to update webhook subscription: %w", err) + } + + return nil, types.UpdateWebhookSubscriptionOutput{ + WebhookSubscription: types.NewWebhookSubscription(subscription), + }, nil +} + +func (r *Resolver) DeleteWebhookSubscriptionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteWebhookSubscriptionInput) (*mcp.CallToolResult, types.DeleteWebhookSubscriptionOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionWebhookSubscriptionDelete) + + prb := r.ProboService(ctx, input.ID) + + err := prb.WebhookSubscriptions.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteWebhookSubscriptionOutput{}, fmt.Errorf("failed to delete webhook subscription: %w", err) + } + + return nil, types.DeleteWebhookSubscriptionOutput{ + DeletedWebhookSubscriptionID: input.ID, + }, nil +} + +func (r *Resolver) ListWebhookEventsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListWebhookEventsInput) (*mcp.CallToolResult, types.ListWebhookEventsOutput, error) { + r.MustAuthorize(ctx, input.WebhookSubscriptionID, probo.ActionWebhookSubscriptionGet) + + prb := r.ProboService(ctx, input.WebhookSubscriptionID) + + pageOrderBy := page.OrderBy[coredata.WebhookEventOrderField]{ + Field: coredata.WebhookEventOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.WebhookEventOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + page, err := prb.WebhookSubscriptions.ListEventsForSubscriptionID(ctx, input.WebhookSubscriptionID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list webhook events: %w", err)) + } + + return nil, types.NewListWebhookEventsOutput(page), nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 5b205b61e..fbb3ab954 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -5950,6 +5950,284 @@ components: format: date-time description: Update timestamp + WebhookEventType: + type: string + enum: + - "meeting:created" + - "meeting:updated" + - "meeting:deleted" + - "vendor:created" + - "vendor:updated" + - "vendor:deleted" + - "user:created" + - "user:updated" + - "user:deleted" + - "obligation:created" + - "obligation:updated" + - "obligation:deleted" + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.WebhookEventType + + WebhookEventStatus: + type: string + enum: + - PENDING + - SUCCEEDED + - FAILED + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.WebhookEventStatus + + WebhookSubscriptionOrderField: + type: string + enum: + - CREATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.WebhookSubscriptionOrderField + + WebhookSubscriptionOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/WebhookSubscriptionOrderField" + description: Webhook subscription order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Order direction + + WebhookEventOrderField: + type: string + enum: + - CREATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.WebhookEventOrderField + + WebhookEventOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/WebhookEventOrderField" + description: Webhook event order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Order direction + + WebhookSubscription: + type: object + required: + - id + - organization_id + - endpoint_url + - selected_events + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Webhook subscription ID + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + endpoint_url: + type: string + description: The HTTPS endpoint URL that receives webhook events + selected_events: + type: array + items: + $ref: "#/components/schemas/WebhookEventType" + description: List of event types this subscription listens to + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + + WebhookEvent: + type: object + required: + - id + - webhook_subscription_id + - status + - created_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Webhook event ID + webhook_subscription_id: + $ref: "#/components/schemas/GID" + description: Webhook subscription ID + status: + $ref: "#/components/schemas/WebhookEventStatus" + description: Delivery status + response: + anyOf: + - type: string + - type: "null" + description: HTTP response body from the endpoint + created_at: + type: string + format: date-time + description: Creation timestamp + + ListWebhookSubscriptionsInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + order_by: + $ref: "#/components/schemas/WebhookSubscriptionOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListWebhookSubscriptionsOutput: + type: object + required: + - webhook_subscriptions + properties: + webhook_subscriptions: + type: array + items: + $ref: "#/components/schemas/WebhookSubscription" + description: List of webhook subscriptions + next_cursor: + anyOf: + - $ref: "#/components/schemas/CursorKey" + - type: "null" + description: Next page cursor + + GetWebhookSubscriptionInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Webhook subscription ID + + GetWebhookSubscriptionOutput: + type: object + required: + - webhook_subscription + properties: + webhook_subscription: + $ref: "#/components/schemas/WebhookSubscription" + + CreateWebhookSubscriptionInput: + type: object + required: + - organization_id + - endpoint_url + - selected_events + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + endpoint_url: + type: string + description: The HTTPS endpoint URL that receives webhook events + selected_events: + type: array + items: + $ref: "#/components/schemas/WebhookEventType" + description: List of event types to subscribe to + + CreateWebhookSubscriptionOutput: + type: object + required: + - webhook_subscription + properties: + webhook_subscription: + $ref: "#/components/schemas/WebhookSubscription" + + UpdateWebhookSubscriptionInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Webhook subscription ID + endpoint_url: + type: string + description: The HTTPS endpoint URL that receives webhook events + selected_events: + type: array + items: + $ref: "#/components/schemas/WebhookEventType" + description: List of event types to subscribe to + + UpdateWebhookSubscriptionOutput: + type: object + required: + - webhook_subscription + properties: + webhook_subscription: + $ref: "#/components/schemas/WebhookSubscription" + + DeleteWebhookSubscriptionInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Webhook subscription ID + + DeleteWebhookSubscriptionOutput: + type: object + required: + - deleted_webhook_subscription_id + properties: + deleted_webhook_subscription_id: + $ref: "#/components/schemas/GID" + description: Deleted webhook subscription ID + + ListWebhookEventsInput: + type: object + required: + - webhook_subscription_id + properties: + webhook_subscription_id: + $ref: "#/components/schemas/GID" + description: Webhook subscription ID + order_by: + $ref: "#/components/schemas/WebhookEventOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListWebhookEventsOutput: + type: object + required: + - webhook_events + properties: + webhook_events: + type: array + items: + $ref: "#/components/schemas/WebhookEvent" + description: List of webhook events + next_cursor: + anyOf: + - $ref: "#/components/schemas/CursorKey" + - type: "null" + description: Next page cursor + ListMeetingsInput: type: object required: @@ -8845,3 +9123,55 @@ tools: $ref: "#/components/schemas/ListAuditLogEntriesInput" outputSchema: $ref: "#/components/schemas/ListAuditLogEntriesOutput" + - name: listWebhookSubscriptions + description: List all webhook subscriptions for the organization + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListWebhookSubscriptionsInput" + outputSchema: + $ref: "#/components/schemas/ListWebhookSubscriptionsOutput" + - name: getWebhookSubscription + description: Get a webhook subscription by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetWebhookSubscriptionInput" + outputSchema: + $ref: "#/components/schemas/GetWebhookSubscriptionOutput" + - name: createWebhookSubscription + description: Create a new webhook subscription for the organization. The endpoint URL must use HTTPS. Selected events determine which events trigger webhook deliveries. + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/CreateWebhookSubscriptionInput" + outputSchema: + $ref: "#/components/schemas/CreateWebhookSubscriptionOutput" + - name: updateWebhookSubscription + description: Update a webhook subscription's endpoint URL or selected events + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateWebhookSubscriptionInput" + outputSchema: + $ref: "#/components/schemas/UpdateWebhookSubscriptionOutput" + - name: deleteWebhookSubscription + description: Delete a webhook subscription + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteWebhookSubscriptionInput" + outputSchema: + $ref: "#/components/schemas/DeleteWebhookSubscriptionOutput" + - name: listWebhookEvents + description: List webhook delivery events for a subscription. Shows delivery status (PENDING, SUCCEEDED, FAILED) and response details. + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListWebhookEventsInput" + outputSchema: + $ref: "#/components/schemas/ListWebhookEventsOutput" diff --git a/pkg/server/api/mcp/v1/types/webhook_subscription.go b/pkg/server/api/mcp/v1/types/webhook_subscription.go new file mode 100644 index 000000000..0cd6fe44b --- /dev/null +++ b/pkg/server/api/mcp/v1/types/webhook_subscription.go @@ -0,0 +1,88 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "encoding/json" + + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewWebhookSubscription(w *coredata.WebhookSubscription) *WebhookSubscription { + events := make([]coredata.WebhookEventType, len(w.SelectedEvents)) + copy(events, w.SelectedEvents) + + return &WebhookSubscription{ + ID: w.ID, + OrganizationID: w.OrganizationID, + EndpointURL: w.EndpointURL, + SelectedEvents: events, + CreatedAt: w.CreatedAt, + UpdatedAt: w.UpdatedAt, + } +} + +func NewListWebhookSubscriptionsOutput(p *page.Page[*coredata.WebhookSubscription, coredata.WebhookSubscriptionOrderField]) ListWebhookSubscriptionsOutput { + subscriptions := make([]*WebhookSubscription, 0, len(p.Data)) + for _, w := range p.Data { + subscriptions = append(subscriptions, NewWebhookSubscription(w)) + } + + var nextCursor *page.CursorKey + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListWebhookSubscriptionsOutput{ + NextCursor: nextCursor, + WebhookSubscriptions: subscriptions, + } +} + +func NewWebhookEvent(e *coredata.WebhookEvent) *WebhookEvent { + var response *string + if len(e.Response) > 0 && string(e.Response) != "null" { + s := string(json.RawMessage(e.Response)) + response = &s + } + + return &WebhookEvent{ + ID: e.ID, + WebhookSubscriptionID: e.WebhookSubscriptionID, + Status: e.Status, + Response: response, + CreatedAt: e.CreatedAt, + } +} + +func NewListWebhookEventsOutput(p *page.Page[*coredata.WebhookEvent, coredata.WebhookEventOrderField]) ListWebhookEventsOutput { + events := make([]*WebhookEvent, 0, len(p.Data)) + for _, e := range p.Data { + events = append(events, NewWebhookEvent(e)) + } + + var nextCursor *page.CursorKey + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListWebhookEventsOutput{ + NextCursor: nextCursor, + WebhookEvents: events, + } +}