fixing lint issues

This commit is contained in:
priyanshu.solanki
2025-12-16 17:52:10 -07:00
parent a15ac7360d
commit 57f3697dd5
19 changed files with 408 additions and 707 deletions

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { permissions, workflowMcpServer, workspace } from '@sim/db/schema'
import { eq, and, sql } from 'drizzle-orm'
import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -12,24 +12,24 @@ export const dynamic = 'force-dynamic'
/**
* GET - Discover all published MCP servers available to the authenticated user
*
*
* This endpoint allows external MCP clients to discover available servers
* using just their API key, without needing to know workspace IDs.
*
*
* Authentication: API Key (X-API-Key header) or Session
*
*
* Returns all published MCP servers from workspaces the user has access to.
*/
export async function GET(request: NextRequest) {
try {
// Authenticate the request
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json(
{
success: false,
error: 'Authentication required. Provide X-API-Key header with your Sim API key.'
{
success: false,
error: 'Authentication required. Provide X-API-Key header with your Sim API key.',
},
{ status: 401 }
)
@@ -41,14 +41,9 @@ export async function GET(request: NextRequest) {
const userWorkspacePermissions = await db
.select({ entityId: permissions.entityId })
.from(permissions)
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, 'workspace')
)
)
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
const workspaceIds = userWorkspacePermissions.map(w => w.entityId)
const workspaceIds = userWorkspacePermissions.map((w) => w.entityId)
if (workspaceIds.length === 0) {
return NextResponse.json({
@@ -87,7 +82,7 @@ export async function GET(request: NextRequest) {
const baseUrl = getBaseUrl()
// Format response with connection URLs
const formattedServers = servers.map(server => ({
const formattedServers = servers.map((server) => ({
id: server.id,
name: server.name,
description: server.description,
@@ -119,7 +114,7 @@ export async function GET(request: NextRequest) {
body: '{"jsonrpc":"2.0","id":1,"method":"tools/list"}',
},
callTool: {
method: 'POST',
method: 'POST',
body: '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"TOOL_NAME","arguments":{}}}',
},
},

View File

@@ -86,10 +86,7 @@ async function validateServer(serverId: string) {
/**
* GET - Server info and capabilities (MCP initialize)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<RouteParams> }
) {
export async function GET(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { serverId } = await params
try {
@@ -122,10 +119,7 @@ export async function GET(
/**
* POST - Handle MCP JSON-RPC requests
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<RouteParams> }
) {
export async function POST(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { serverId } = await params
try {
@@ -151,10 +145,9 @@ export async function POST(
const rpcRequest = body as JsonRpcRequest
if (rpcRequest.jsonrpc !== '2.0' || !rpcRequest.method) {
return NextResponse.json(
createJsonRpcError(rpcRequest?.id || 0, -32600, 'Invalid Request'),
{ status: 400 }
)
return NextResponse.json(createJsonRpcError(rpcRequest?.id || 0, -32600, 'Invalid Request'), {
status: 400,
})
}
// Handle different MCP methods
@@ -178,7 +171,9 @@ export async function POST(
case 'tools/call': {
// Get the API key from the request to forward to the workflow execute call
const apiKey = request.headers.get('X-API-Key') || request.headers.get('Authorization')?.replace('Bearer ', '')
const apiKey =
request.headers.get('X-API-Key') ||
request.headers.get('Authorization')?.replace('Bearer ', '')
return handleToolsCall(rpcRequest, serverId, auth.userId, server.workspaceId, apiKey)
}
@@ -193,10 +188,7 @@ export async function POST(
}
} catch (error) {
logger.error('Error handling MCP request:', error)
return NextResponse.json(
createJsonRpcError(0, -32603, 'Internal error'),
{ status: 500 }
)
return NextResponse.json(createJsonRpcError(0, -32603, 'Internal error'), { status: 500 })
}
}
@@ -236,15 +228,12 @@ async function handleToolsList(
},
}))
return NextResponse.json(
createJsonRpcResponse(rpcRequest.id, { tools: mcpTools })
)
return NextResponse.json(createJsonRpcResponse(rpcRequest.id, { tools: mcpTools }))
} catch (error) {
logger.error('Error listing tools:', error)
return NextResponse.json(
createJsonRpcError(rpcRequest.id, -32603, 'Failed to list tools'),
{ status: 500 }
)
return NextResponse.json(createJsonRpcError(rpcRequest.id, -32603, 'Failed to list tools'), {
status: 500,
})
}
}
@@ -259,7 +248,9 @@ async function handleToolsCall(
apiKey?: string | null
): Promise<NextResponse> {
try {
const params = rpcRequest.params as { name: string; arguments?: Record<string, unknown> } | undefined
const params = rpcRequest.params as
| { name: string; arguments?: Record<string, unknown> }
| undefined
if (!params?.name) {
return NextResponse.json(
@@ -318,7 +309,7 @@ async function handleToolsCall(
const executeHeaders: Record<string, string> = {
'Content-Type': 'application/json',
}
// Forward the API key for authentication
if (apiKey) {
executeHeaders['X-API-Key'] = apiKey
@@ -362,9 +353,8 @@ async function handleToolsCall(
)
} catch (error) {
logger.error('Error calling tool:', error)
return NextResponse.json(
createJsonRpcError(rpcRequest.id, -32603, 'Tool execution failed'),
{ status: 500 }
)
return NextResponse.json(createJsonRpcError(rpcRequest.id, -32603, 'Tool execution failed'), {
status: 500,
})
}
}

View File

@@ -3,8 +3,8 @@ import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('WorkflowMcpSSE')
@@ -54,10 +54,7 @@ async function validateServer(serverId: string) {
* GET - SSE endpoint for MCP protocol
* This establishes a Server-Sent Events connection for bidirectional MCP communication
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<RouteParams> }
) {
export async function GET(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { serverId } = await params
try {
@@ -160,10 +157,7 @@ export async function GET(
* POST - Handle messages sent to the SSE endpoint
* This is used for the message channel in MCP streamable-http transport
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<RouteParams> }
) {
export async function POST(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { serverId } = await params
try {
@@ -224,7 +218,9 @@ export async function POST(
case 'tools/call': {
// Get the API key from the request to forward to the workflow execute call
const apiKey = request.headers.get('X-API-Key') || request.headers.get('Authorization')?.replace('Bearer ', '')
const apiKey =
request.headers.get('X-API-Key') ||
request.headers.get('Authorization')?.replace('Bearer ', '')
return handleToolsCall(message, serverId, userId, workspaceId, apiKey)
}
@@ -258,10 +254,7 @@ export async function POST(
/**
* Handle tools/list method
*/
async function handleToolsList(
id: string | number,
serverId: string
): Promise<NextResponse> {
async function handleToolsList(id: string | number, serverId: string): Promise<NextResponse> {
const tools = await db
.select({
toolName: workflowMcpTool.toolName,
@@ -369,7 +362,7 @@ async function handleToolsCall(
const executeHeaders: Record<string, string> = {
'Content-Type': 'application/json',
}
// Forward the API key for authentication
if (apiKey) {
executeHeaders['X-API-Key'] = apiKey

View File

@@ -54,7 +54,9 @@ export const POST = withMcpAuth<RouteParams>('admin')(
if (tools.length === 0) {
return createMcpErrorResponse(
new Error('Cannot publish server without any tools. Add at least one workflow as a tool first.'),
new Error(
'Cannot publish server without any tools. Add at least one workflow as a tool first.'
),
'Server has no tools',
400
)

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
import { and, eq, sql } from 'drizzle-orm'
import { and, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
@@ -51,7 +51,9 @@ export const GET = withMcpAuth<RouteParams>('read')(
.from(workflowMcpTool)
.where(eq(workflowMcpTool.serverId, serverId))
logger.info(`[${requestId}] Found workflow MCP server: ${server.name} with ${tools.length} tools`)
logger.info(
`[${requestId}] Found workflow MCP server: ${server.name} with ${tools.length} tools`
)
return createMcpSuccessResponse({ server, tools })
} catch (error) {

View File

@@ -20,13 +20,15 @@ interface RouteParams {
* Tool names should be lowercase, alphanumeric with underscores.
*/
function sanitizeToolName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\s_-]/g, '')
.replace(/[\s-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 64) || 'workflow_tool'
return (
name
.toLowerCase()
.replace(/[^a-z0-9\s_-]/g, '')
.replace(/[\s-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 64) || 'workflow_tool'
)
}
/**
@@ -217,7 +219,9 @@ export const POST = withMcpAuth<RouteParams>('write')(
// Generate tool name and description
const toolName = body.toolName?.trim() || sanitizeToolName(workflowRecord.name)
const toolDescription =
body.toolDescription?.trim() || workflowRecord.description || `Execute ${workflowRecord.name} workflow`
body.toolDescription?.trim() ||
workflowRecord.description ||
`Execute ${workflowRecord.name} workflow`
// Create the tool
const toolId = crypto.randomUUID()

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
import { and, eq, sql } from 'drizzle-orm'
import { workflowMcpServer } from '@sim/db/schema'
import { eq, sql } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'

View File

@@ -138,11 +138,11 @@ async function syncMcpToolsOnDeploy(workflowId: string, requestId: string): Prom
const hasStart = await hasValidStartBlock(workflowId)
if (!hasStart) {
// No start block - remove all MCP tools for this workflow
await db
.delete(workflowMcpTool)
.where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(`[${requestId}] Removed ${tools.length} MCP tool(s) - workflow no longer has a start block: ${workflowId}`)
await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(
`[${requestId}] Removed ${tools.length} MCP tool(s) - workflow no longer has a start block: ${workflowId}`
)
return
}

View File

@@ -133,11 +133,11 @@ async function syncMcpToolsOnVersionActivate(
// Check if the activated version has a valid start block
if (!hasValidStartBlockInState(versionState)) {
// No start block - remove all MCP tools for this workflow
await db
.delete(workflowMcpTool)
.where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(`[${requestId}] Removed ${tools.length} MCP tool(s) - activated version has no start block: ${workflowId}`)
await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(
`[${requestId}] Removed ${tools.length} MCP tool(s) - activated version has no start block: ${workflowId}`
)
return
}
@@ -153,7 +153,9 @@ async function syncMcpToolsOnVersionActivate(
})
.where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow version activation: ${workflowId}`)
logger.info(
`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow version activation: ${workflowId}`
)
} catch (error) {
logger.error(`[${requestId}] Error syncing MCP tools on version activate:`, error)
// Don't throw - this is a non-critical operation

View File

@@ -135,11 +135,11 @@ async function syncMcpToolsOnRevert(
// Check if the reverted version has a valid start block
if (!hasValidStartBlockInState(versionState)) {
// No start block - remove all MCP tools for this workflow
await db
.delete(workflowMcpTool)
.where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(`[${requestId}] Removed ${tools.length} MCP tool(s) - reverted version has no start block: ${workflowId}`)
await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(
`[${requestId}] Removed ${tools.length} MCP tool(s) - reverted version has no start block: ${workflowId}`
)
return
}
@@ -155,7 +155,9 @@ async function syncMcpToolsOnRevert(
})
.where(eq(workflowMcpTool.workflowId, workflowId))
logger.info(`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow revert: ${workflowId}`)
logger.info(
`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow revert: ${workflowId}`
)
} catch (error) {
logger.error(`[${requestId}] Error syncing MCP tools on revert:`, error)
// Don't throw - this is a non-critical operation

View File

@@ -1,7 +1,15 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { AlertTriangle, Check, ChevronDown, ChevronRight, Plus, RefreshCw, Server, Trash2 } from 'lucide-react'
import {
AlertTriangle,
ChevronDown,
ChevronRight,
Plus,
RefreshCw,
Server,
Trash2,
} from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
@@ -42,20 +50,24 @@ interface McpToolDeployProps {
* Sanitize a workflow name to be a valid MCP tool name.
*/
function sanitizeToolName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\s_-]/g, '')
.replace(/[\s-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 64) || 'workflow_tool'
return (
name
.toLowerCase()
.replace(/[^a-z0-9\s_-]/g, '')
.replace(/[\s-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 64) || 'workflow_tool'
)
}
/**
* Extract input format from workflow blocks using SubBlockStore
* The actual input format values are stored in useSubBlockStore, not directly in the block structure
*/
function extractInputFormat(blocks: Record<string, unknown>): Array<{ name: string; type: string }> {
function extractInputFormat(
blocks: Record<string, unknown>
): Array<{ name: string; type: string }> {
// Find the starter block
for (const [blockId, block] of Object.entries(blocks)) {
if (!block || typeof block !== 'object') continue
@@ -67,7 +79,7 @@ function extractInputFormat(blocks: Record<string, unknown>): Array<{ name: stri
if (
blockType === 'starter' ||
blockType === 'start' ||
blockType === 'start_trigger' || // This is the unified start block type
blockType === 'start_trigger' || // This is the unified start block type
blockType === 'api' ||
blockType === 'api_trigger' ||
blockType === 'input_trigger'
@@ -325,7 +337,10 @@ function ToolOnServer({
</Badge>
)}
{needsUpdate && (
<Badge variant='outline' className='border-amber-500/50 bg-amber-500/10 text-[10px] text-amber-500'>
<Badge
variant='outline'
className='border-amber-500/50 bg-amber-500/10 text-[10px] text-amber-500'
>
<AlertTriangle className='mr-[4px] h-[10px] w-[10px]' />
Needs Update
</Badge>
@@ -339,7 +354,12 @@ function ToolOnServer({
disabled={updateToolMutation.isPending}
className='h-[24px] px-[8px] text-[11px] text-amber-500 hover:text-amber-600'
>
<RefreshCw className={cn('mr-[4px] h-[10px] w-[10px]', updateToolMutation.isPending && 'animate-spin')} />
<RefreshCw
className={cn(
'mr-[4px] h-[10px] w-[10px]',
updateToolMutation.isPending && 'animate-spin'
)}
/>
{updateToolMutation.isPending ? 'Updating...' : 'Update'}
</Button>
)}
@@ -354,14 +374,18 @@ function ToolOnServer({
</div>
{showDetails && (
<div className='border-t border-[var(--border)] px-[10px] py-[8px]'>
<div className='border-[var(--border)] border-t px-[10px] py-[8px]'>
<div className='flex flex-col gap-[6px]'>
<div className='flex items-center justify-between'>
<span className='text-[11px] text-[var(--text-muted)]'>Tool Name</span>
<span className='font-mono text-[11px] text-[var(--text-secondary)]'>{tool.toolName}</span>
<span className='font-mono text-[11px] text-[var(--text-secondary)]'>
{tool.toolName}
</span>
</div>
<div className='flex items-start justify-between gap-[8px]'>
<span className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>Description</span>
<span className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
Description
</span>
<span className='text-right text-[11px] text-[var(--text-secondary)]'>
{tool.toolDescription || '—'}
</span>
@@ -399,12 +423,16 @@ export function McpToolDeploy({
const params = useParams()
const workspaceId = params.workspaceId as string
const { data: servers = [], isLoading: isLoadingServers, refetch: refetchServers } = useWorkflowMcpServers(workspaceId)
const {
data: servers = [],
isLoading: isLoadingServers,
refetch: refetchServers,
} = useWorkflowMcpServers(workspaceId)
const addToolMutation = useAddWorkflowMcpTool()
// Get workflow blocks
const blocks = useWorkflowStore((state) => state.blocks)
// Find the starter block ID to subscribe to its inputFormat changes
const starterBlockId = useMemo(() => {
for (const [blockId, block] of Object.entries(blocks)) {
@@ -414,7 +442,7 @@ export function McpToolDeploy({
if (
blockType === 'starter' ||
blockType === 'start' ||
blockType === 'start_trigger' || // This is the unified start block type
blockType === 'start_trigger' || // This is the unified start block type
blockType === 'api' ||
blockType === 'api_trigger' ||
blockType === 'input_trigger'
@@ -428,7 +456,7 @@ export function McpToolDeploy({
// Subscribe to the inputFormat value in SubBlockStore for reactivity
// Use workflowId prop directly (not activeWorkflowId from registry) to ensure we get the correct workflow's data
const subBlockValues = useSubBlockStore((state) =>
workflowId ? state.workflowValues[workflowId] ?? {} : {}
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
)
// Extract and normalize input format - now reactive to SubBlockStore changes
@@ -436,7 +464,7 @@ export function McpToolDeploy({
// First try to get from SubBlockStore (where runtime values are stored)
if (starterBlockId && subBlockValues[starterBlockId]) {
const inputFormatValue = subBlockValues[starterBlockId].inputFormat
if (Array.isArray(inputFormatValue) && inputFormatValue.length > 0) {
const filtered = inputFormatValue
.filter(
@@ -461,7 +489,7 @@ export function McpToolDeploy({
if (starterBlockId && blocks[starterBlockId]) {
const startBlock = blocks[starterBlockId]
const subBlocksValue = startBlock?.subBlocks?.inputFormat?.value as unknown
if (Array.isArray(subBlocksValue) && subBlocksValue.length > 0) {
const validFields: Array<{ name: string; type: string }> = []
for (const field of subBlocksValue) {
@@ -497,22 +525,27 @@ export function McpToolDeploy({
const [showParameterSchema, setShowParameterSchema] = useState(false)
// Track tools data from each server using state instead of hooks in a loop
const [serverToolsMap, setServerToolsMap] = useState<Record<string, { tool: WorkflowMcpTool | null; isLoading: boolean }>>({})
const [serverToolsMap, setServerToolsMap] = useState<
Record<string, { tool: WorkflowMcpTool | null; isLoading: boolean }>
>({})
// Stable callback to handle tool data from ServerToolsQuery components
const handleServerToolData = useCallback((serverId: string, tool: WorkflowMcpTool | null, isLoading: boolean) => {
setServerToolsMap((prev) => {
// Only update if data has changed to prevent infinite loops
const existing = prev[serverId]
if (existing?.tool?.id === tool?.id && existing?.isLoading === isLoading) {
return prev
}
return {
...prev,
[serverId]: { tool, isLoading },
}
})
}, [])
const handleServerToolData = useCallback(
(serverId: string, tool: WorkflowMcpTool | null, isLoading: boolean) => {
setServerToolsMap((prev) => {
// Only update if data has changed to prevent infinite loops
const existing = prev[serverId]
if (existing?.tool?.id === tool?.id && existing?.isLoading === isLoading) {
return prev
}
return {
...prev,
[serverId]: { tool, isLoading },
}
})
},
[]
)
// Find which servers already have this workflow as a tool and get the tool info
const serversWithThisWorkflow = useMemo(() => {
@@ -555,7 +588,7 @@ export function McpToolDeploy({
setSelectedServer(null)
setToolName('')
setToolDescription('')
// Refetch servers to update tool count
refetchServers()
onAddedToServer?.()
@@ -576,18 +609,21 @@ export function McpToolDeploy({
onAddedToServer,
])
const handleToolChanged = useCallback((removedServerId?: string) => {
// If a tool was removed from a specific server, clear just that entry
// The ServerToolsQuery component will re-query and update the map
if (removedServerId) {
setServerToolsMap((prev) => {
const next = { ...prev }
delete next[removedServerId]
return next
})
}
refetchServers()
}, [refetchServers])
const handleToolChanged = useCallback(
(removedServerId?: string) => {
// If a tool was removed from a specific server, clear just that entry
// The ServerToolsQuery component will re-query and update the map
if (removedServerId) {
setServerToolsMap((prev) => {
const next = { ...prev }
delete next[removedServerId]
return next
})
}
refetchServers()
},
[refetchServers]
)
const availableServers = useMemo(() => {
const addedServerIds = new Set(serversWithThisWorkflow.map((s) => s.server.id))
@@ -646,7 +682,8 @@ export function McpToolDeploy({
<div className='flex flex-col gap-[4px]'>
<p className='text-[13px] text-[var(--text-secondary)]'>
Add this workflow as an MCP tool to make it callable by external MCP clients like Cursor or Claude Desktop.
Add this workflow as an MCP tool to make it callable by external MCP clients like Cursor
or Claude Desktop.
</p>
</div>
@@ -655,8 +692,8 @@ export function McpToolDeploy({
<div className='flex items-center gap-[8px] rounded-[6px] border border-amber-500/30 bg-amber-500/10 px-[10px] py-[8px]'>
<AlertTriangle className='h-[14px] w-[14px] flex-shrink-0 text-amber-500' />
<p className='text-[12px] text-amber-600 dark:text-amber-400'>
{toolsNeedingUpdate.length} server{toolsNeedingUpdate.length > 1 ? 's have' : ' has'} outdated tool
definitions. Click "Update" on each to sync with current parameters.
{toolsNeedingUpdate.length} server{toolsNeedingUpdate.length > 1 ? 's have' : ' has'}{' '}
outdated tool definitions. Click "Update" on each to sync with current parameters.
</p>
</div>
)}
@@ -677,12 +714,13 @@ export function McpToolDeploy({
Current Tool Parameters ({inputFormat.length})
</Label>
</button>
{showParameterSchema && (
<div className='rounded-[6px] border bg-[var(--surface-4)] p-[12px]'>
{inputFormat.length === 0 ? (
<p className='text-[12px] text-[var(--text-muted)]'>
No parameters defined. Add input fields in the Starter block to define tool parameters.
No parameters defined. Add input fields in the Starter block to define tool
parameters.
</p>
) : (
<div className='flex flex-col gap-[8px]'>

View File

@@ -179,7 +179,9 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='flex flex-col gap-[16px]'>
<div className='flex flex-col gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>Server Name</span>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
Server Name
</span>
<p className='text-[14px] text-[var(--text-secondary)]'>{server.name}</p>
</div>
@@ -317,7 +319,9 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
Are you sure you want to remove{' '}
<span className='font-medium text-[var(--text-primary)]'>{toolToDelete?.toolName}</span>{' '}
<span className='font-medium text-[var(--text-primary)]'>
{toolToDelete?.toolName}
</span>{' '}
from this server?
</p>
</ModalBody>
@@ -464,10 +468,14 @@ export function WorkflowMcpServers() {
<div className='rounded-[8px] border bg-[var(--surface-3)] p-[12px]'>
<div className='flex flex-col gap-[12px]'>
<div className='flex flex-col gap-[6px]'>
<label className='font-medium text-[13px] text-[var(--text-secondary)]'>
<label
htmlFor='mcp-server-name'
className='font-medium text-[13px] text-[var(--text-secondary)]'
>
Server Name
</label>
<EmcnInput
id='mcp-server-name'
placeholder='e.g., My Workflow Tools'
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
@@ -476,13 +484,19 @@ export function WorkflowMcpServers() {
</div>
<div className='flex flex-col gap-[6px]'>
<label className='font-medium text-[13px] text-[var(--text-secondary)]'>
<label
htmlFor='mcp-server-description'
className='font-medium text-[13px] text-[var(--text-secondary)]'
>
Description (optional)
</label>
<EmcnInput
id='mcp-server-description'
placeholder='Describe what this server provides...'
value={formData.description}
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
onChange={(e) =>
setFormData((prev) => ({ ...prev, description: e.target.value }))
}
className='h-9'
/>
</div>
@@ -551,7 +565,8 @@ export function WorkflowMcpServers() {
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{serverToDelete?.name}</span>?{' '}
<span className='font-medium text-[var(--text-primary)]'>{serverToDelete?.name}</span>
?{' '}
<span className='text-[var(--text-error)]'>
This will remove all tools and cannot be undone.
</span>

View File

@@ -208,11 +208,14 @@ export function useUpdateWorkflowMcpServer() {
name,
description,
}: UpdateWorkflowMcpServerParams) => {
const response = await fetch(`/api/mcp/workflow-servers/${serverId}?workspaceId=${workspaceId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description }),
})
const response = await fetch(
`/api/mcp/workflow-servers/${serverId}?workspaceId=${workspaceId}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description }),
}
)
const data = await response.json()

View File

@@ -1,7 +1,14 @@
import { env } from '@/lib/core/config/env'
import type { TokenBucketConfig } from './storage'
export type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp' | 'api-endpoint'
export type TriggerType =
| 'api'
| 'webhook'
| 'schedule'
| 'manual'
| 'chat'
| 'mcp'
| 'api-endpoint'
export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint'

View File

@@ -49,13 +49,15 @@ function mapFieldTypeToJsonSchemaType(fieldType: string | undefined): string {
* Tool names should be lowercase, alphanumeric with underscores.
*/
export function sanitizeToolName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\s_-]/g, '')
.replace(/[\s-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 64) || 'workflow_tool'
return (
name
.toLowerCase()
.replace(/[^a-z0-9\s_-]/g, '')
.replace(/[\s-]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 64) || 'workflow_tool'
)
}
/**

View File

@@ -60,7 +60,9 @@ const parseTriggerArrayFromURL = (value: string | null): TriggerType[] => {
if (!value) return []
return value
.split(',')
.filter((t): t is TriggerType => ['chat', 'api', 'webhook', 'manual', 'schedule', 'mcp'].includes(t))
.filter((t): t is TriggerType =>
['chat', 'api', 'webhook', 'manual', 'schedule', 'mcp'].includes(t)
)
}
const parseStringArrayFromURL = (value: string | null): string[] => {

View File

@@ -166,7 +166,15 @@ export type TimeRange =
| 'Past 30 days'
| 'All time'
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all'
export type TriggerType = 'chat' | 'api' | 'webhook' | 'manual' | 'schedule' | 'mcp' | 'all' | string
export type TriggerType =
| 'chat'
| 'api'
| 'webhook'
| 'manual'
| 'schedule'
| 'mcp'
| 'all'
| string
export interface FilterState {
// Workspace context

File diff suppressed because it is too large Load Diff

View File

@@ -864,4 +864,4 @@
"breakpoints": true
}
]
}
}