chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,836 @@
|
||||
import { db } from '@sim/db'
|
||||
import { chat, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import {
|
||||
performCreateWorkflowMcpTool,
|
||||
performDeleteWorkflowMcpTool,
|
||||
performUpdateWorkflowMcpTool,
|
||||
} from '@/lib/mcp/orchestration'
|
||||
import { getDeployedWorkflowInputFormat } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import {
|
||||
applyDescriptionOverrides,
|
||||
generateToolInputSchema,
|
||||
getMeaningfulWorkflowDescription,
|
||||
sanitizeToolName,
|
||||
} from '@/lib/mcp/workflow-tool-schema'
|
||||
import {
|
||||
performChatDeploy,
|
||||
performChatUndeploy,
|
||||
performFullDeploy,
|
||||
performFullUndeploy,
|
||||
} from '@/lib/workflows/orchestration'
|
||||
import { checkChatAccess, checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils'
|
||||
import { ensureWorkflowAccess } from '../access'
|
||||
import type { DeployApiParams, DeployChatParams, DeployMcpParams } from '../param-types'
|
||||
|
||||
function buildWorkflowApiEndpoint(baseUrl: string, workflowId: string): string {
|
||||
return `${baseUrl}/api/workflows/${workflowId}/execute`
|
||||
}
|
||||
|
||||
function buildWorkflowApiConfig(baseUrl: string, apiEndpoint: string) {
|
||||
return {
|
||||
endpoint: apiEndpoint,
|
||||
authentication: {
|
||||
type: 'api_key',
|
||||
acceptedHeaders: ['X-API-Key: YOUR_API_KEY', 'Authorization: Bearer YOUR_API_KEY'],
|
||||
},
|
||||
modes: {
|
||||
sync: {
|
||||
method: 'POST',
|
||||
transport: 'json',
|
||||
stream: false,
|
||||
body: { input: { key: 'value' } },
|
||||
},
|
||||
stream: {
|
||||
method: 'POST',
|
||||
transport: 'sse',
|
||||
stream: true,
|
||||
body: { stream: true, input: { key: 'value' } },
|
||||
},
|
||||
async: {
|
||||
method: 'POST',
|
||||
transport: 'json',
|
||||
stream: false,
|
||||
headers: { 'X-Execution-Mode': 'async' },
|
||||
body: { input: { key: 'value' } },
|
||||
jobStatusEndpointTemplate: `${baseUrl}/api/jobs/{jobId}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkflowApiExamples(baseUrl: string, apiEndpoint: string) {
|
||||
return {
|
||||
sync: `curl -X POST "${apiEndpoint}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: YOUR_API_KEY" \\
|
||||
-d '{"input":{"key":"value"}}'`,
|
||||
stream: `curl -N -X POST "${apiEndpoint}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: YOUR_API_KEY" \\
|
||||
-d '{"stream":true,"input":{"key":"value"}}'`,
|
||||
async: `curl -X POST "${apiEndpoint}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: YOUR_API_KEY" \\
|
||||
-H "X-Execution-Mode: async" \\
|
||||
-d '{"input":{"key":"value"}}'`,
|
||||
poll: `curl "${baseUrl}/api/jobs/JOB_ID" \\
|
||||
-H "X-API-Key: YOUR_API_KEY"`,
|
||||
}
|
||||
}
|
||||
|
||||
function buildMcpClientExamples(serverName: string, serverUrl: string) {
|
||||
return {
|
||||
cursor: {
|
||||
mcpServers: {
|
||||
[serverName]: {
|
||||
url: serverUrl,
|
||||
headers: { 'X-API-Key': 'YOUR_API_KEY' },
|
||||
},
|
||||
},
|
||||
},
|
||||
claudeCode: `claude mcp add ${serverName} --url "${serverUrl}" --header "X-API-Key: YOUR_API_KEY"`,
|
||||
claudeDesktop: {
|
||||
mcpServers: {
|
||||
[serverName]: {
|
||||
command: 'npx',
|
||||
args: ['-y', 'mcp-remote', serverUrl, '--header', 'X-API-Key:YOUR_API_KEY'],
|
||||
},
|
||||
},
|
||||
},
|
||||
vscode: {
|
||||
mcp: {
|
||||
servers: {
|
||||
[serverName]: {
|
||||
type: 'http',
|
||||
url: serverUrl,
|
||||
headers: { 'X-API-Key': 'YOUR_API_KEY' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeployApi(
|
||||
params: DeployApiParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
const action = params.action === 'undeploy' ? 'undeploy' : 'deploy'
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'admin'
|
||||
)
|
||||
|
||||
if (action === 'undeploy') {
|
||||
const result = await performFullUndeploy({ workflowId, userId: context.userId })
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to undeploy workflow' }
|
||||
}
|
||||
const baseUrl = getBaseUrl()
|
||||
const apiEndpoint = buildWorkflowApiEndpoint(baseUrl, workflowId)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
isDeployed: false,
|
||||
apiEndpoint,
|
||||
baseUrl,
|
||||
deploymentType: 'api',
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: false,
|
||||
endpoint: apiEndpoint,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
api: buildWorkflowApiConfig(baseUrl, apiEndpoint),
|
||||
},
|
||||
examples: {
|
||||
api: {
|
||||
curl: buildWorkflowApiExamples(baseUrl, apiEndpoint),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const versionDescription = params.versionDescription?.trim()
|
||||
if (!versionDescription) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'versionDescription is required when deploying. Provide a concise summary of what changed in this deployment version (call diff_workflows with ref1 "live" and ref2 "draft" if unsure what changed).',
|
||||
}
|
||||
}
|
||||
|
||||
const versionName = params.versionName?.trim()
|
||||
if (!versionName) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'versionName is required when deploying. Provide a short human-readable label for this deployment version.',
|
||||
}
|
||||
}
|
||||
|
||||
const result = await performFullDeploy({
|
||||
workflowId,
|
||||
userId: context.userId,
|
||||
workflowName: workflowRecord.name || undefined,
|
||||
versionDescription,
|
||||
versionName,
|
||||
})
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to deploy workflow' }
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const apiEndpoint = buildWorkflowApiEndpoint(baseUrl, workflowId)
|
||||
const apiConfig = buildWorkflowApiConfig(baseUrl, apiEndpoint)
|
||||
const apiExamples = buildWorkflowApiExamples(baseUrl, apiEndpoint)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
isDeployed: true,
|
||||
deployedAt: result.deployedAt,
|
||||
version: result.version,
|
||||
apiEndpoint,
|
||||
baseUrl,
|
||||
deploymentType: 'api',
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: true,
|
||||
endpoint: apiEndpoint,
|
||||
deployedAt: result.deployedAt,
|
||||
version: result.version,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
api: apiConfig,
|
||||
},
|
||||
examples: {
|
||||
api: {
|
||||
curl: apiExamples,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeployChat(
|
||||
params: DeployChatParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
|
||||
const action = params.action === 'undeploy' ? 'undeploy' : 'deploy'
|
||||
if (action === 'undeploy') {
|
||||
const baseUrl = getBaseUrl()
|
||||
const apiEndpoint = buildWorkflowApiEndpoint(baseUrl, workflowId)
|
||||
const apiConfig = buildWorkflowApiConfig(baseUrl, apiEndpoint)
|
||||
const apiExamples = buildWorkflowApiExamples(baseUrl, apiEndpoint)
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt)))
|
||||
.limit(1)
|
||||
if (!existing.length) {
|
||||
return { success: false, error: 'No active chat deployment found for this workflow' }
|
||||
}
|
||||
const { hasAccess, workspaceId: chatWorkspaceId } = await checkChatAccess(
|
||||
existing[0].id,
|
||||
context.userId
|
||||
)
|
||||
if (!hasAccess) {
|
||||
return { success: false, error: 'Unauthorized chat access' }
|
||||
}
|
||||
const undeployResult = await performChatUndeploy({
|
||||
chatId: existing[0].id,
|
||||
userId: context.userId,
|
||||
workspaceId: chatWorkspaceId,
|
||||
})
|
||||
if (!undeployResult.success) {
|
||||
return { success: false, error: undeployResult.error || 'Failed to undeploy chat' }
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
success: true,
|
||||
action: 'undeploy',
|
||||
isDeployed: true,
|
||||
isChatDeployed: false,
|
||||
deploymentType: 'chat',
|
||||
apiEndpoint,
|
||||
baseUrl,
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: true,
|
||||
endpoint: apiEndpoint,
|
||||
},
|
||||
chat: {
|
||||
isDeployed: false,
|
||||
identifier: existing[0].identifier,
|
||||
title: existing[0].title,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
api: apiConfig,
|
||||
chat: {
|
||||
identifier: existing[0].identifier,
|
||||
title: existing[0].title,
|
||||
description: existing[0].description || '',
|
||||
authType: existing[0].authType,
|
||||
allowedEmails: (existing[0].allowedEmails as string[]) || [],
|
||||
outputConfigs:
|
||||
(existing[0].outputConfigs as Array<{ blockId: string; path: string }>) || [],
|
||||
welcomeMessage:
|
||||
(existing[0].customizations as { welcomeMessage?: string } | null)
|
||||
?.welcomeMessage || 'Hi there! How can I help you today?',
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
api: {
|
||||
curl: apiExamples,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const { hasAccess, workflow: workflowRecord } = await checkWorkflowAccessForChatCreation(
|
||||
workflowId,
|
||||
context.userId
|
||||
)
|
||||
if (!hasAccess || !workflowRecord) {
|
||||
return { success: false, error: 'Workflow not found or access denied' }
|
||||
}
|
||||
|
||||
const [existingDeployment] = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(and(eq(chat.workflowId, workflowId), isNull(chat.archivedAt)))
|
||||
.limit(1)
|
||||
|
||||
const identifier = String(params.identifier || existingDeployment?.identifier || '').trim()
|
||||
const title = String(params.title || existingDeployment?.title || '').trim()
|
||||
if (!identifier || !title) {
|
||||
return { success: false, error: 'Chat identifier and title are required' }
|
||||
}
|
||||
|
||||
const versionDescription = params.versionDescription?.trim()
|
||||
if (!versionDescription) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'versionDescription is required when deploying. Provide a concise summary of what changed in this deployment version (distinct from the chat-facing description; call diff_workflows with ref1 "live" and ref2 "draft" if unsure).',
|
||||
}
|
||||
}
|
||||
|
||||
const versionName = params.versionName?.trim()
|
||||
if (!versionName) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'versionName is required when deploying. Provide a short human-readable label for this deployment version (distinct from the chat title).',
|
||||
}
|
||||
}
|
||||
|
||||
const identifierPattern = /^[a-z0-9-]+$/
|
||||
if (!identifierPattern.test(identifier)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Identifier can only contain lowercase letters, numbers, and hyphens',
|
||||
}
|
||||
}
|
||||
|
||||
const existingIdentifier = await db
|
||||
.select()
|
||||
.from(chat)
|
||||
.where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
|
||||
.limit(1)
|
||||
if (existingIdentifier.length > 0 && existingIdentifier[0].id !== existingDeployment?.id) {
|
||||
return { success: false, error: 'Identifier already in use' }
|
||||
}
|
||||
|
||||
const existingCustomizations =
|
||||
(existingDeployment?.customizations as
|
||||
| { primaryColor?: string; welcomeMessage?: string; imageUrl?: string }
|
||||
| undefined) || {}
|
||||
const resolvedDescription = String(params.description || existingDeployment?.description || '')
|
||||
const resolvedAuthType = (params.authType || existingDeployment?.authType || 'public') as
|
||||
| 'public'
|
||||
| 'password'
|
||||
| 'email'
|
||||
| 'sso'
|
||||
const resolvedAllowedEmails =
|
||||
params.allowedEmails || (existingDeployment?.allowedEmails as string[]) || []
|
||||
const resolvedOutputConfigs = (params.outputConfigs ||
|
||||
existingDeployment?.outputConfigs ||
|
||||
[]) as Array<{
|
||||
blockId: string
|
||||
path: string
|
||||
}>
|
||||
const welcomeMessage =
|
||||
typeof params.welcomeMessage === 'string'
|
||||
? params.welcomeMessage
|
||||
: params.customizations?.welcomeMessage || existingCustomizations.welcomeMessage
|
||||
const imageUrl =
|
||||
params.customizations?.imageUrl ||
|
||||
params.customizations?.iconUrl ||
|
||||
existingCustomizations.imageUrl
|
||||
|
||||
const result = await performChatDeploy({
|
||||
workflowId,
|
||||
userId: context.userId,
|
||||
identifier,
|
||||
title,
|
||||
description: resolvedDescription,
|
||||
versionDescription,
|
||||
versionName,
|
||||
customizations: {
|
||||
primaryColor:
|
||||
params.customizations?.primaryColor ||
|
||||
existingCustomizations.primaryColor ||
|
||||
'var(--brand-hover)',
|
||||
welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
|
||||
...(imageUrl ? { imageUrl } : {}),
|
||||
},
|
||||
authType: resolvedAuthType,
|
||||
password: params.password,
|
||||
allowedEmails: resolvedAllowedEmails,
|
||||
outputConfigs: resolvedOutputConfigs,
|
||||
workspaceId: workflowRecord.workspaceId,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to deploy chat' }
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const apiEndpoint = buildWorkflowApiEndpoint(baseUrl, workflowId)
|
||||
const apiConfig = buildWorkflowApiConfig(baseUrl, apiEndpoint)
|
||||
const apiExamples = buildWorkflowApiExamples(baseUrl, apiEndpoint)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
success: true,
|
||||
action: 'deploy',
|
||||
isDeployed: true,
|
||||
isChatDeployed: true,
|
||||
identifier,
|
||||
chatUrl: result.chatUrl,
|
||||
apiEndpoint,
|
||||
baseUrl,
|
||||
deployedAt: result.deployedAt || null,
|
||||
version: result.version,
|
||||
deploymentType: 'chat',
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: true,
|
||||
endpoint: apiEndpoint,
|
||||
deployedAt: result.deployedAt || null,
|
||||
version: result.version,
|
||||
},
|
||||
chat: {
|
||||
isDeployed: true,
|
||||
identifier,
|
||||
chatUrl: result.chatUrl,
|
||||
title,
|
||||
description: resolvedDescription,
|
||||
authType: resolvedAuthType,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
api: apiConfig,
|
||||
chat: {
|
||||
identifier,
|
||||
chatUrl: result.chatUrl,
|
||||
title,
|
||||
description: resolvedDescription,
|
||||
authType: resolvedAuthType,
|
||||
allowedEmails: resolvedAllowedEmails,
|
||||
outputConfigs: resolvedOutputConfigs,
|
||||
welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?',
|
||||
primaryColor:
|
||||
params.customizations?.primaryColor ||
|
||||
existingCustomizations.primaryColor ||
|
||||
'var(--brand-hover)',
|
||||
...(imageUrl ? { imageUrl } : {}),
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
chat: {
|
||||
open: result.chatUrl,
|
||||
},
|
||||
api: {
|
||||
curl: apiExamples,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeployMcp(
|
||||
params: DeployMcpParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'admin'
|
||||
)
|
||||
const workspaceId = workflowRecord.workspaceId
|
||||
if (!workspaceId) {
|
||||
return { success: false, error: 'workspaceId is required' }
|
||||
}
|
||||
|
||||
const serverId = params.serverId
|
||||
if (!serverId) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'serverId is required. Use list_workspace_mcp_servers to get available servers.',
|
||||
}
|
||||
}
|
||||
const [serverRecord] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowMcpServer.id, serverId),
|
||||
eq(workflowMcpServer.workspaceId, workspaceId),
|
||||
isNull(workflowMcpServer.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
if (!serverRecord) {
|
||||
return { success: false, error: 'MCP server not found in this workspace' }
|
||||
}
|
||||
|
||||
// Handle undeploy action — remove workflow from MCP server
|
||||
if (params.action === 'undeploy') {
|
||||
const [existingTool] = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowMcpTool.serverId, serverId),
|
||||
eq(workflowMcpTool.workflowId, workflowId),
|
||||
isNull(workflowMcpTool.archivedAt)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingTool) {
|
||||
return { success: false, error: 'Workflow is not deployed to this MCP server' }
|
||||
}
|
||||
|
||||
const deleteResult = await performDeleteWorkflowMcpTool({
|
||||
serverId,
|
||||
toolId: existingTool.id,
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
if (!deleteResult.success) {
|
||||
return { success: false, error: deleteResult.error || 'Failed to undeploy MCP tool' }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
action: 'undeploy',
|
||||
removed: true,
|
||||
deploymentType: 'mcp',
|
||||
deploymentStatus: {
|
||||
mcp: {
|
||||
isDeployed: false,
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflowRecord.isDeployed) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow must be deployed before adding as an MCP tool. Use deploy_api first.',
|
||||
}
|
||||
}
|
||||
|
||||
const existingTool = await db
|
||||
.select()
|
||||
.from(workflowMcpTool)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowMcpTool.serverId, serverId),
|
||||
eq(workflowMcpTool.workflowId, workflowId),
|
||||
isNull(workflowMcpTool.archivedAt)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const toolName = sanitizeToolName(
|
||||
params.toolName || workflowRecord.name || `workflow_${workflowId}`
|
||||
)
|
||||
const toolDescription =
|
||||
params.toolDescription?.trim() ||
|
||||
getMeaningfulWorkflowDescription(workflowRecord.description, workflowRecord.name) ||
|
||||
`Execute ${workflowRecord.name} workflow`
|
||||
/**
|
||||
* Parameter names/types come from the workflow's deployed input trigger; this tool only sets
|
||||
* per-parameter descriptions, sent as sparse overrides. The materialized schema is echoed in the
|
||||
* response for the model's reference.
|
||||
*/
|
||||
const inputFormat = await getDeployedWorkflowInputFormat(workflowId)
|
||||
const parameterDescriptionOverrides = Object.fromEntries(
|
||||
(params.parameterDescriptions ?? [])
|
||||
.filter((entry) => entry && typeof entry.name === 'string' && entry.name.trim() !== '')
|
||||
.map((entry) => [entry.name.trim(), (entry.description ?? '').trim()])
|
||||
.filter(([, description]) => description !== '')
|
||||
)
|
||||
const parameterSchema = applyDescriptionOverrides(
|
||||
generateToolInputSchema(inputFormat),
|
||||
parameterDescriptionOverrides
|
||||
)
|
||||
const baseUrl = getBaseUrl()
|
||||
const mcpServerUrl = `${baseUrl}/api/mcp/serve/${serverId}`
|
||||
const apiEndpoint = buildWorkflowApiEndpoint(baseUrl, workflowId)
|
||||
const clientExamples = buildMcpClientExamples(serverRecord.name, mcpServerUrl)
|
||||
|
||||
if (existingTool.length > 0) {
|
||||
const toolId = existingTool[0].id
|
||||
const updateResult = await performUpdateWorkflowMcpTool({
|
||||
serverId,
|
||||
toolId,
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
parameterDescriptionOverrides,
|
||||
})
|
||||
if (!updateResult.success || !updateResult.tool) {
|
||||
return { success: false, error: updateResult.error || 'Failed to update MCP tool' }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
toolId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
updated: true,
|
||||
mcpServerUrl,
|
||||
baseUrl,
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
deploymentType: 'mcp',
|
||||
apiEndpoint,
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: true,
|
||||
endpoint: apiEndpoint,
|
||||
},
|
||||
mcp: {
|
||||
isDeployed: true,
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
toolId,
|
||||
toolName,
|
||||
updated: true,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
mcp: {
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
serverUrl: mcpServerUrl,
|
||||
toolId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
parameterSchema,
|
||||
authentication: {
|
||||
type: 'api_key',
|
||||
header: 'X-API-Key: YOUR_API_KEY',
|
||||
},
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
mcp: clientExamples,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const createResult = await performCreateWorkflowMcpTool({
|
||||
serverId,
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
workflowId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
parameterDescriptionOverrides,
|
||||
})
|
||||
if (!createResult.success || !createResult.tool) {
|
||||
return { success: false, error: createResult.error || 'Failed to deploy MCP tool' }
|
||||
}
|
||||
const toolId = createResult.tool.id
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
toolId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
updated: false,
|
||||
mcpServerUrl,
|
||||
baseUrl,
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
deploymentType: 'mcp',
|
||||
apiEndpoint,
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: true,
|
||||
endpoint: apiEndpoint,
|
||||
},
|
||||
mcp: {
|
||||
isDeployed: true,
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
toolId,
|
||||
toolName,
|
||||
updated: false,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
mcp: {
|
||||
serverId,
|
||||
serverName: serverRecord.name,
|
||||
serverUrl: mcpServerUrl,
|
||||
toolId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
parameterSchema,
|
||||
authentication: {
|
||||
type: 'api_key',
|
||||
header: 'X-API-Key: YOUR_API_KEY',
|
||||
},
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
mcp: clientExamples,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeRedeploy(
|
||||
params: { workflowId?: string; versionDescription?: string; versionName?: string },
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId || context.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
const versionDescription = params.versionDescription?.trim()
|
||||
if (!versionDescription) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'versionDescription is required. Provide a concise summary of what changed in this deployment version (call diff_workflows with ref1 "live" and ref2 "draft" if unsure what changed).',
|
||||
}
|
||||
}
|
||||
const versionName = params.versionName?.trim()
|
||||
if (!versionName) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'versionName is required. Provide a short human-readable label for this deployment version.',
|
||||
}
|
||||
}
|
||||
await ensureWorkflowAccess(workflowId, context.userId, 'admin')
|
||||
|
||||
const result = await performFullDeploy({
|
||||
workflowId,
|
||||
userId: context.userId,
|
||||
versionDescription,
|
||||
versionName,
|
||||
})
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to redeploy workflow' }
|
||||
}
|
||||
const baseUrl = getBaseUrl()
|
||||
const apiEndpoint = buildWorkflowApiEndpoint(baseUrl, workflowId)
|
||||
const apiConfig = buildWorkflowApiConfig(baseUrl, apiEndpoint)
|
||||
const apiExamples = buildWorkflowApiExamples(baseUrl, apiEndpoint)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
isDeployed: true,
|
||||
deployedAt: result.deployedAt || null,
|
||||
version: result.version,
|
||||
apiEndpoint,
|
||||
baseUrl,
|
||||
deploymentType: 'api',
|
||||
deploymentStatus: {
|
||||
api: {
|
||||
isDeployed: true,
|
||||
endpoint: apiEndpoint,
|
||||
deployedAt: result.deployedAt || null,
|
||||
version: result.version,
|
||||
},
|
||||
},
|
||||
deploymentConfig: {
|
||||
api: apiConfig,
|
||||
},
|
||||
examples: {
|
||||
api: {
|
||||
curl: apiExamples,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: toError(error).message }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user