Skip to content

Commit 6c10f31

Browse files
author
priyanshu.solanki
committed
using official mcp sdk and added description fields
1 parent 896e967 commit 6c10f31

File tree

12 files changed

+681
-488
lines changed

12 files changed

+681
-488
lines changed

apps/sim/app/api/mcp/serve/[serverId]/sse/route.ts

Lines changed: 75 additions & 293 deletions
Large diffs are not rendered by default.

apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,24 @@ import { createLogger } from '@/lib/logs/console/logger'
66
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
77
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
88
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
9-
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils'
9+
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
10+
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
1011

1112
const logger = createLogger('WorkflowMcpToolsAPI')
1213

14+
/**
15+
* Check if a workflow has a valid start block by loading from database
16+
*/
17+
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
18+
try {
19+
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
20+
return hasValidStartBlockInState(normalizedData)
21+
} catch (error) {
22+
logger.warn('Error checking for start block:', error)
23+
return false
24+
}
25+
}
26+
1327
export const dynamic = 'force-dynamic'
1428

1529
interface RouteParams {

apps/sim/app/api/workflows/[id]/deploy/route.ts

Lines changed: 21 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,30 @@ import { and, desc, eq } from 'drizzle-orm'
33
import type { NextRequest } from 'next/server'
44
import { generateRequestId } from '@/lib/core/utils/request'
55
import { createLogger } from '@/lib/logs/console/logger'
6+
import {
7+
extractInputFormatFromBlocks,
8+
generateToolInputSchema,
9+
} from '@/lib/mcp/workflow-tool-schema'
610
import { deployWorkflow, loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
11+
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
712
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
8-
import {
9-
hasValidStartBlock,
10-
isValidStartBlockType,
11-
} from '@/lib/workflows/triggers/trigger-utils'
1213
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
1314

1415
const logger = createLogger('WorkflowDeployAPI')
1516

17+
/**
18+
* Check if a workflow has a valid start block by loading from database
19+
*/
20+
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
21+
try {
22+
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
23+
return hasValidStartBlockInState(normalizedData)
24+
} catch (error) {
25+
logger.warn('Error checking for start block:', error)
26+
return false
27+
}
28+
}
29+
1630
export const dynamic = 'force-dynamic'
1731
export const runtime = 'nodejs'
1832

@@ -26,58 +40,12 @@ async function generateMcpToolSchema(workflowId: string): Promise<Record<string,
2640
return { type: 'object', properties: {} }
2741
}
2842

29-
// Find the start block
30-
const startBlock = Object.values(normalizedData.blocks).find((block: any) => {
31-
return isValidStartBlockType(block?.type)
32-
}) as any
33-
34-
if (!startBlock?.subBlocks?.inputFormat?.value) {
43+
const inputFormat = extractInputFormatFromBlocks(normalizedData.blocks)
44+
if (!inputFormat || inputFormat.length === 0) {
3545
return { type: 'object', properties: {} }
3646
}
3747

38-
const inputFormat = startBlock.subBlocks.inputFormat.value
39-
if (!Array.isArray(inputFormat) || inputFormat.length === 0) {
40-
return { type: 'object', properties: {} }
41-
}
42-
43-
const properties: Record<string, { type: string; description: string }> = {}
44-
const required: string[] = []
45-
46-
for (const field of inputFormat) {
47-
if (!field?.name || typeof field.name !== 'string' || !field.name.trim()) continue
48-
49-
const fieldName = field.name.trim()
50-
let jsonType = 'string'
51-
switch (field.type) {
52-
case 'number':
53-
jsonType = 'number'
54-
break
55-
case 'boolean':
56-
jsonType = 'boolean'
57-
break
58-
case 'object':
59-
jsonType = 'object'
60-
break
61-
case 'array':
62-
case 'files':
63-
jsonType = 'array'
64-
break
65-
default:
66-
jsonType = 'string'
67-
}
68-
69-
properties[fieldName] = {
70-
type: jsonType,
71-
description: fieldName,
72-
}
73-
required.push(fieldName)
74-
}
75-
76-
return {
77-
type: 'object',
78-
properties,
79-
required: required.length > 0 ? required : undefined,
80-
}
48+
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
8149
} catch (error) {
8250
logger.warn('Error generating MCP tool schema:', error)
8351
return { type: 'object', properties: {} }

apps/sim/app/api/workflows/[id]/deployments/[version]/activate/route.ts

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { and, eq } from 'drizzle-orm'
33
import type { NextRequest } from 'next/server'
44
import { generateRequestId } from '@/lib/core/utils/request'
55
import { createLogger } from '@/lib/logs/console/logger'
6-
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
76
import {
8-
hasValidStartBlockInState,
9-
isValidStartBlockType,
10-
} from '@/lib/workflows/triggers/trigger-utils'
7+
extractInputFormatFromBlocks,
8+
generateToolInputSchema,
9+
} from '@/lib/mcp/workflow-tool-schema'
10+
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
11+
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
1112
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
1213

1314
const logger = createLogger('WorkflowActivateDeploymentAPI')
@@ -24,58 +25,12 @@ function generateMcpToolSchemaFromState(state: any): Record<string, unknown> {
2425
return { type: 'object', properties: {} }
2526
}
2627

27-
// Find the start block in the deployed state
28-
const startBlock = Object.values(state.blocks).find((block: any) => {
29-
return isValidStartBlockType(block?.type)
30-
}) as any
31-
32-
if (!startBlock?.subBlocks?.inputFormat?.value) {
33-
return { type: 'object', properties: {} }
34-
}
35-
36-
const inputFormat = startBlock.subBlocks.inputFormat.value
37-
if (!Array.isArray(inputFormat) || inputFormat.length === 0) {
28+
const inputFormat = extractInputFormatFromBlocks(state.blocks)
29+
if (!inputFormat || inputFormat.length === 0) {
3830
return { type: 'object', properties: {} }
3931
}
4032

41-
const properties: Record<string, { type: string; description: string }> = {}
42-
const required: string[] = []
43-
44-
for (const field of inputFormat) {
45-
if (!field?.name || typeof field.name !== 'string' || !field.name.trim()) continue
46-
47-
const fieldName = field.name.trim()
48-
let jsonType = 'string'
49-
switch (field.type) {
50-
case 'number':
51-
jsonType = 'number'
52-
break
53-
case 'boolean':
54-
jsonType = 'boolean'
55-
break
56-
case 'object':
57-
jsonType = 'object'
58-
break
59-
case 'array':
60-
case 'files':
61-
jsonType = 'array'
62-
break
63-
default:
64-
jsonType = 'string'
65-
}
66-
67-
properties[fieldName] = {
68-
type: jsonType,
69-
description: fieldName,
70-
}
71-
required.push(fieldName)
72-
}
73-
74-
return {
75-
type: 'object',
76-
properties,
77-
required: required.length > 0 ? required : undefined,
78-
}
33+
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
7934
} catch (error) {
8035
logger.warn('Error generating MCP tool schema from state:', error)
8136
return { type: 'object', properties: {} }

apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import type { NextRequest } from 'next/server'
44
import { env } from '@/lib/core/config/env'
55
import { generateRequestId } from '@/lib/core/utils/request'
66
import { createLogger } from '@/lib/logs/console/logger'
7+
import {
8+
extractInputFormatFromBlocks,
9+
generateToolInputSchema,
10+
} from '@/lib/mcp/workflow-tool-schema'
711
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
12+
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
813
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
9-
import {
10-
hasValidStartBlockInState,
11-
isValidStartBlockType,
12-
} from '@/lib/workflows/triggers/trigger-utils'
1314
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
1415

1516
const logger = createLogger('RevertToDeploymentVersionAPI')
@@ -26,58 +27,12 @@ function generateMcpToolSchemaFromState(state: any): Record<string, unknown> {
2627
return { type: 'object', properties: {} }
2728
}
2829

29-
// Find the start block in the deployed state
30-
const startBlock = Object.values(state.blocks).find((block: any) => {
31-
return isValidStartBlockType(block?.type)
32-
}) as any
33-
34-
if (!startBlock?.subBlocks?.inputFormat?.value) {
30+
const inputFormat = extractInputFormatFromBlocks(state.blocks)
31+
if (!inputFormat || inputFormat.length === 0) {
3532
return { type: 'object', properties: {} }
3633
}
3734

38-
const inputFormat = startBlock.subBlocks.inputFormat.value
39-
if (!Array.isArray(inputFormat) || inputFormat.length === 0) {
40-
return { type: 'object', properties: {} }
41-
}
42-
43-
const properties: Record<string, { type: string; description: string }> = {}
44-
const required: string[] = []
45-
46-
for (const field of inputFormat) {
47-
if (!field?.name || typeof field.name !== 'string' || !field.name.trim()) continue
48-
49-
const fieldName = field.name.trim()
50-
let jsonType = 'string'
51-
switch (field.type) {
52-
case 'number':
53-
jsonType = 'number'
54-
break
55-
case 'boolean':
56-
jsonType = 'boolean'
57-
break
58-
case 'object':
59-
jsonType = 'object'
60-
break
61-
case 'array':
62-
case 'files':
63-
jsonType = 'array'
64-
break
65-
default:
66-
jsonType = 'string'
67-
}
68-
69-
properties[fieldName] = {
70-
type: jsonType,
71-
description: fieldName,
72-
}
73-
required.push(fieldName)
74-
}
75-
76-
return {
77-
type: 'object',
78-
properties,
79-
required: required.length > 0 ? required : undefined,
80-
}
35+
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
8136
} catch (error) {
8237
logger.warn('Error generating MCP tool schema from state:', error)
8338
return { type: 'object', properties: {} }

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp-tool/mcp-tool.tsx

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,18 @@ function extractInputFormat(
118118

119119
/**
120120
* Generate JSON Schema from input format using the shared utility
121+
* Optionally applies custom descriptions from the UI
121122
*/
122123
function generateParameterSchema(
123-
inputFormat: Array<{ name: string; type: string }>
124+
inputFormat: Array<{ name: string; type: string }>,
125+
customDescriptions?: Record<string, string>
124126
): Record<string, unknown> {
125-
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
127+
// Convert to InputFormatField with descriptions
128+
const fieldsWithDescriptions = inputFormat.map((field) => ({
129+
...field,
130+
description: customDescriptions?.[field.name]?.trim() || undefined,
131+
}))
132+
return generateToolInputSchema(fieldsWithDescriptions) as unknown as Record<string, unknown>
126133
}
127134

128135
/**
@@ -461,13 +468,18 @@ export function McpToolDeploy({
461468
return extractInputFormat(blocks)
462469
}, [starterBlockId, subBlockValues, blocks])
463470

464-
const parameterSchema = useMemo(() => generateParameterSchema(inputFormat), [inputFormat])
465-
466471
const [selectedServer, setSelectedServer] = useState<WorkflowMcpServer | null>(null)
467472
const [toolName, setToolName] = useState('')
468473
const [toolDescription, setToolDescription] = useState('')
469474
const [showServerSelector, setShowServerSelector] = useState(false)
470475
const [showParameterSchema, setShowParameterSchema] = useState(false)
476+
// Track custom descriptions for each parameter
477+
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
478+
479+
const parameterSchema = useMemo(
480+
() => generateParameterSchema(inputFormat, parameterDescriptions),
481+
[inputFormat, parameterDescriptions]
482+
)
471483

472484
// Track tools data from each server using state instead of hooks in a loop
473485
const [serverToolsMap, setServerToolsMap] = useState<
@@ -509,6 +521,32 @@ export function McpToolDeploy({
509521
return serversWithThisWorkflow.filter(({ tool }) => hasParameterMismatch(tool, inputFormat))
510522
}, [serversWithThisWorkflow, inputFormat])
511523

524+
// Load existing parameter descriptions from the first deployed tool
525+
useEffect(() => {
526+
if (serversWithThisWorkflow.length > 0) {
527+
const existingTool = serversWithThisWorkflow[0].tool
528+
const schema = existingTool.parameterSchema as Record<string, unknown> | undefined
529+
const properties = schema?.properties as Record<string, { description?: string }> | undefined
530+
531+
if (properties) {
532+
const descriptions: Record<string, string> = {}
533+
for (const [name, prop] of Object.entries(properties)) {
534+
// Only use description if it differs from the field name (i.e., it's custom)
535+
if (
536+
prop.description &&
537+
prop.description !== name &&
538+
prop.description !== 'Array of file objects'
539+
) {
540+
descriptions[name] = prop.description
541+
}
542+
}
543+
if (Object.keys(descriptions).length > 0) {
544+
setParameterDescriptions(descriptions)
545+
}
546+
}
547+
}
548+
}, [serversWithThisWorkflow])
549+
512550
// Reset form when selected server changes
513551
useEffect(() => {
514552
if (selectedServer) {
@@ -668,17 +706,33 @@ export function McpToolDeploy({
668706
parameters.
669707
</p>
670708
) : (
671-
<div className='flex flex-col gap-[8px]'>
709+
<div className='flex flex-col gap-[12px]'>
672710
{inputFormat.map((field, index) => (
673-
<div key={index} className='flex items-center justify-between'>
674-
<span className='font-mono text-[12px] text-[var(--text-primary)]'>
675-
{field.name}
676-
</span>
677-
<Badge variant='outline' className='text-[10px]'>
678-
{field.type}
679-
</Badge>
711+
<div key={index} className='flex flex-col gap-[6px]'>
712+
<div className='flex items-center justify-between'>
713+
<span className='font-mono text-[12px] text-[var(--text-primary)]'>
714+
{field.name}
715+
</span>
716+
<Badge variant='outline' className='text-[10px]'>
717+
{field.type}
718+
</Badge>
719+
</div>
720+
<EmcnInput
721+
value={parameterDescriptions[field.name] || ''}
722+
onChange={(e) =>
723+
setParameterDescriptions((prev) => ({
724+
...prev,
725+
[field.name]: e.target.value,
726+
}))
727+
}
728+
placeholder={`Describe what "${field.name}" is for...`}
729+
className='h-[32px] text-[12px]'
730+
/>
680731
</div>
681732
))}
733+
<p className='text-[11px] text-[var(--text-muted)]'>
734+
Descriptions help MCP clients understand what each parameter is for.
735+
</p>
682736
</div>
683737
)}
684738
</div>

0 commit comments

Comments
 (0)