mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fixing lint issues
This commit is contained in:
@@ -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":{}}}',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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[] => {
|
||||
|
||||
@@ -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
@@ -864,4 +864,4 @@
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user