Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions docs/mcp/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ Enable or disable a flow.
| `flowId` | string | Yes | The flow ID |
| `status` | string | Yes | `ENABLED` or `DISABLED` |

### ap_delete_flow

Permanently delete a flow and all its versions. This cannot be undone.

| Input | Type | Required | Description |
|-------|------|----------|-------------|
| `flowId` | string | Yes | The ID of the flow to delete |

### ap_lock_and_publish

Publish the current draft of a flow. Validates all steps are configured.
Expand Down
3 changes: 3 additions & 0 deletions packages/server/api/src/app/helper/logger/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { loggerRedact } from '@activepieces/server-utils'
import { FastifyBaseLogger } from 'fastify'
import pino, { Level, Logger } from 'pino'
import { AppSystemProp, environmentVariables } from '../system/system-props'
Expand All @@ -12,6 +13,7 @@ export const pinoLogging = {
if (pretty) {
return pino({
level,
redact: loggerRedact,
transport: {
target: 'pino-pretty',
options: {
Expand All @@ -38,6 +40,7 @@ export const pinoLogging = {

return pino({
level,
redact: loggerRedact,
transport: {
targets: defaultTargets,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { loggerRedact } from '@activepieces/server-utils'
import { Level, pino, TransportTargetOptions } from 'pino'
import { AppSystemProp, environmentVariables } from '../../system/system-props'
import { TransportProvider } from './transport-provider'
Expand All @@ -14,6 +15,7 @@ export const betterstackTransport: TransportProvider = {

return pino({
level,
redact: loggerRedact,
transport: {
targets: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { loggerRedact } from '@activepieces/server-utils'
import * as HyperDX from '@hyperdx/node-opentelemetry'
import { Level, pino, transport, TransportTargetOptions } from 'pino'
import { AppSystemProp, environmentVariables } from '../../system/system-props'
Expand All @@ -16,7 +17,7 @@ export const hyperdxTransport: TransportProvider = {
})

return pino(
{ level, mixin: HyperDX.getPinoMixinFunction },
{ level, redact: loggerRedact, mixin: HyperDX.getPinoMixinFunction },
transport({
targets: [
HyperDX.getPinoTransport(level, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { loggerRedact } from '@activepieces/server-utils'
import { Level, pino, TransportTargetOptions } from 'pino'
import 'pino-loki'
import { AppSystemProp, environmentVariables } from '../../system/system-props'
Expand All @@ -15,6 +16,7 @@ export const lokiTransport: TransportProvider = {

return pino({
level,
redact: loggerRedact,
transport: {
targets: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

import { loggerRedact } from '@activepieces/server-utils'
import { Level, pino, transport, TransportTargetOptions } from 'pino'
import { AppSystemProp, environmentVariables } from '../../system/system-props'
import { TransportProvider } from './transport-provider'
Expand All @@ -10,8 +11,8 @@ export const otelTransport: TransportProvider = {
},
createLogger(level: Level, targets: TransportTargetOptions[]) {
return pino(
{ level },
transport({
{ level, redact: loggerRedact },
transport({
targets: [
{
target: 'pino-opentelemetry-transport',
Expand Down
10 changes: 7 additions & 3 deletions packages/server/api/src/app/mcp/tools/ap-build-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ export const apBuildFlowTool = (mcp: ProjectScopedMcpServer, log: FastifyBaseLog
},
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },
execute: async (args) => {
let flowId: string | undefined
const projectId = mcp.projectId
try {
const { flowName, trigger, steps } = buildFlowInput.parse(args)
const project = await projectService(log).getOneOrThrow(mcp.projectId)
const project = await projectService(log).getOneOrThrow(projectId)
const platformId = project.platformId
const projectId = mcp.projectId

const triggerAuthError = mcpUtils.validateAuth(trigger.auth)
if (triggerAuthError) {
Expand All @@ -82,7 +83,7 @@ export const apBuildFlowTool = (mcp: ProjectScopedMcpServer, log: FastifyBaseLog
projectId,
request: { displayName: flowName, projectId },
})
const flowId = flow.id
flowId = flow.id

const triggerInput = {
...(trigger.input ?? {}),
Expand Down Expand Up @@ -146,6 +147,9 @@ export const apBuildFlowTool = (mcp: ProjectScopedMcpServer, log: FastifyBaseLog
return { content: [{ type: 'text', text: `⚠️ Flow "${flowName}" created (id: ${flowId}) with ${allSteps.length} ${stepWord} (${validCount} valid, ${invalidSteps.length} invalid: ${invalidSteps.join(', ')}).${skippedHint} Use ap_update_step or ap_update_trigger to fix.` }] }
}
catch (err) {
if (flowId) {
await flowService(log).delete({ id: flowId, projectId }).catch(() => undefined)
}
return mcpUtils.mcpToolError('Failed to build flow', err)
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ export const apChangeFlowStatusTool = (mcp: ProjectScopedMcpServer, log: Fastify
}
}

if (status === FlowStatus.DISABLED && flow.status === FlowStatus.DISABLED) {
return {
content: [{
type: 'text',
text: `✅ Flow "${flow.version.displayName}" is already disabled.`,
}],
}
}

const operation: FlowOperationRequest = {
type: FlowOperationType.CHANGE_STATUS,
request: { status },
Expand Down
4 changes: 2 additions & 2 deletions packages/server/api/src/app/mcp/tools/ap-create-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export const apCreateFlowTool = (mcp: ProjectScopedMcpServer, log: FastifyBaseLo
permission: Permission.WRITE_FLOW,
description: 'Create a new flow in Activepieces',
inputSchema: {
flowName: z.string().describe('The name of the flow'),
flowName: z.string().trim().min(1, 'Flow name cannot be empty').max(255, 'Flow name must be 255 characters or less').describe('The name of the flow'),
},
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },
execute: async (args) => {
const { flowName } = z.object({ flowName: z.string() }).parse(args)
const { flowName } = z.object({ flowName: z.string().trim().min(1).max(255) }).parse(args)
try {
const flow = await flowService(log).create({
projectId: mcp.projectId,
Expand Down
37 changes: 37 additions & 0 deletions packages/server/api/src/app/mcp/tools/ap-delete-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { McpToolDefinition, Permission, ProjectScopedMcpServer } from '@activepieces/shared'
import { FastifyBaseLogger } from 'fastify'
import { z } from 'zod'
import { flowService } from '../../flows/flow/flow.service'
import { mcpUtils } from './mcp-utils'

export const apDeleteFlowTool = (mcp: ProjectScopedMcpServer, log: FastifyBaseLogger): McpToolDefinition => {
return {
title: 'ap_delete_flow',
permission: Permission.WRITE_FLOW,
description: 'Permanently delete a flow and all its versions. This cannot be undone.',
inputSchema: {
flowId: z.string().describe('The ID of the flow to delete'),
},
annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: false },
execute: async (args) => {
const { flowId } = z.object({ flowId: z.string() }).parse(args)
try {
const flow = await flowService(log).getOnePopulated({ id: flowId, projectId: mcp.projectId })
const displayName = flow?.version?.displayName ?? flowId
await flowService(log).delete({
id: flowId,
projectId: mcp.projectId,
})
return {
content: [{
type: 'text',
text: `✅ Flow "${displayName}" has been permanently deleted.`,
}],
}
}
catch (err) {
return mcpUtils.mcpToolError('Flow deletion failed', err)
}
},
}
}
41 changes: 41 additions & 0 deletions packages/server/api/src/app/mcp/tools/ap-get-piece-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export const apGetPiecePropsTool = (mcp: ProjectScopedMcpServer, log: FastifyBas
const requiresAuth = component.requireAuth && !isNil(piece.auth)

let authHint: AuthHint | undefined
if (requiresAuth && auth) {
const authOwnership = await validateAuthOwnership({ auth, pieceName: normalized, projectId: mcp.projectId, log })
if (authOwnership) {
return authOwnership
}
}
if (requiresAuth && !auth) {
authHint = await discoverAvailableConnections({ pieceName: normalized, projectId: mcp.projectId, log })
}
Expand Down Expand Up @@ -183,6 +189,41 @@ async function discoverAvailableConnections({ pieceName, projectId, log }: {
}
}

async function validateAuthOwnership({ auth, pieceName, projectId, log }: {
auth: string
pieceName: string
projectId: string
log: FastifyBaseLogger
}): Promise<{ content: [{ type: 'text', text: string }] } | null> {
try {
const project = await projectService(log).getOneOrThrow(projectId)
const connections = await appConnectionService(log).list({
projectId,
platformId: project.platformId,
pieceName,
cursorRequest: null,
scope: undefined,
displayName: undefined,
status: undefined,
limit: 1,
externalIds: [auth],
})
const match = connections.data[0]
if (!match) {
return {
content: [{
type: 'text',
text: `⚠️ Connection "${auth}" does not belong to piece "${pieceName}". Use ap_list_connections to find the correct connection for this piece.`,
}],
}
}
}
catch {
// If lookup fails, proceed anyway — don't block the user
}
return null
}

const { withTimeout } = mcpUtils

const getPiecePropsInput = z.object({
Expand Down
4 changes: 2 additions & 2 deletions packages/server/api/src/app/mcp/tools/ap-list-pieces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ export const apListPiecesTool = (mcp: ProjectScopedMcpServer, log: FastifyBaseLo
name: a.name,
displayName: a.displayName,
description: a.description,
requireAuth: a.requireAuth,
requiresAuth: a.requireAuth,
}))
}
if (params.includeTriggers) {
base.triggers = Object.values(fullPiece.triggers).map(t => ({
name: t.name,
displayName: t.displayName,
description: t.description,
requireAuth: t.requireAuth,
requiresAuth: t.requireAuth,
}))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ export const apResolvePropertyOptionsTool = (mcp: ProjectScopedMcpServer, log: F
}
}

if (Array.isArray(options)) {
const mapped = options.map((o: { label: string, value: unknown }) => ({ label: o.label, value: o.value }))
const optionsArray = extractOptionsArray(options)

if (optionsArray !== null) {
const mapped = optionsArray.map((o: { label: string, value: unknown }) => ({ label: String(o.label ?? ''), value: o.value }))
if (mapped.length === 0) {
return {
content: [{ type: 'text', text: `⚠️ No options found for "${propertyName}". The account may have no items. You may use the value the user provided directly, but the dropdown in the flow editor will appear unset.` }],
Expand All @@ -102,8 +104,9 @@ export const apResolvePropertyOptionsTool = (mcp: ProjectScopedMcpServer, log: F
}
}

log.warn({ propertyName, optionsType: typeof options, options }, 'ap_resolve_property_options: unrecognized options format')
return {
content: [{ type: 'text', text: `⚠️ Unexpected response format for "${propertyName}".` }],
content: [{ type: 'text', text: `⚠️ Could not parse options for "${propertyName}". You may use the value the user provided directly — it may work at runtime.` }],
}
}
catch (err) {
Expand All @@ -116,6 +119,19 @@ export const apResolvePropertyOptionsTool = (mcp: ProjectScopedMcpServer, log: F
}
}

function extractOptionsArray(options: unknown): Array<{ label: string, value: unknown }> | null {
if (Array.isArray(options)) return options

if (isObject(options) && !Array.isArray(options)) {
const obj = options as Record<string, unknown>
if (Array.isArray(obj.options)) {
return obj.options as Array<{ label: string, value: unknown }>
}
}

return null
}

const resolvePropertyOptionsInput = z.object({
pieceName: z.string().describe('The piece name (e.g. "@activepieces/piece-slack").'),
actionOrTriggerName: z.string().describe('The action or trigger name (e.g. "send_channel_message").'),
Expand Down
48 changes: 45 additions & 3 deletions packages/server/api/src/app/mcp/tools/ap-setup-guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,20 @@ async function connectionGuide(mcp: ProjectScopedMcpServer, log: FastifyBaseLogg
}

const authOptions = Array.isArray(rawAuth) ? rawAuth : [rawAuth]

if (authOptions.length > 1) {
const lines: string[] = [`How to connect "${piece.displayName}" (${authOptions.length} methods available):`, '']
for (let i = 0; i < authOptions.length; i++) {
lines.push(`**Option ${i + 1}: ${formatAuthTypeName(authOptions[i].type)}**`)
lines.push(...formatAuthSteps({ auth: authOptions[i], displayName: piece.displayName }))
lines.push('')
}
return { content: [{ type: 'text', text: lines.join('\n') }] }
}

const auth = authOptions[0]
const authType = auth.type
const lines: string[] = [`How to connect "${piece.displayName}":`, '']
if (authOptions.length > 1) {
lines.push(`Note: This piece supports ${authOptions.length} authentication methods. Showing the primary one.`, '')
}

switch (authType) {
case PropertyType.OAUTH2:
Expand Down Expand Up @@ -145,6 +153,40 @@ async function connectionGuide(mcp: ProjectScopedMcpServer, log: FastifyBaseLogg
return { content: [{ type: 'text', text: lines.join('\n') }] }
}

function formatAuthTypeName(type: string): string {
switch (type) {
case PropertyType.OAUTH2: return 'OAuth2'
case PropertyType.SECRET_TEXT: return 'API Key'
case PropertyType.BASIC_AUTH: return 'Basic Auth (username/password)'
case PropertyType.CUSTOM_AUTH: return 'Custom Auth'
default: return type
}
}

function formatAuthSteps({ auth, displayName }: { auth: Record<string, unknown>, displayName: string }): string[] {
const steps: string[] = []
switch (auth.type) {
case PropertyType.OAUTH2:
steps.push(`1. Go to Settings → Connections → "+ New Connection" → "${displayName}"`, '2. Click "Connect" — OAuth popup opens', '3. Log in and authorize')
break
case PropertyType.SECRET_TEXT:
steps.push(`1. Go to Settings → Connections → "+ New Connection" → "${displayName}"`, `2. Enter your API key${'description' in auth && auth.description ? ` (${auth.description})` : ''}`, '3. Click Save')
break
case PropertyType.BASIC_AUTH:
steps.push(`1. Go to Settings → Connections → "+ New Connection" → "${displayName}"`, '2. Enter username and password', '3. Click Save')
break
case PropertyType.CUSTOM_AUTH: {
const props = (auth.props ?? {}) as Record<string, { displayName?: string, required?: boolean }>
const fields = Object.entries(props).map(([key, p]) => ` - ${p.displayName ?? key}${p.required !== false ? ' (required)' : ' (optional)'}`)
steps.push(`1. Go to Settings → Connections → "+ New Connection" → "${displayName}"`, '2. Fill in:', ...fields, '3. Click Save')
break
}
default:
steps.push(`1. Go to Settings → Connections → "+ New Connection" → "${displayName}"`, '2. Follow the on-screen instructions')
}
return steps
}

async function aiProviderGuide(mcp: ProjectScopedMcpServer, log: FastifyBaseLogger): Promise<{ content: [{ type: 'text', text: string }] }> {
const project = await projectService(log).getOneOrThrow(mcp.projectId)
const providers = await aiProviderService(log).listProviders(project.platformId)
Expand Down
Loading
Loading