Skip to content

Commit fce0406

Browse files
authored
fix: mcp tool call issue (opentiny#1587)
1 parent 14b77fd commit fce0406

18 files changed

Lines changed: 529 additions & 67 deletions

File tree

packages/canvas/DesignCanvas/src/mcp/tools/addNode.ts

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
import { z } from 'zod'
2-
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
2+
import { useCanvas, useMaterial } from '@opentiny/tiny-engine-meta-register'
33
import { utils } from '@opentiny/tiny-engine-utils'
44

55
const { validateParams } = utils
66

7-
type NodeSchema = z.ZodObject<{
8-
componentName: z.ZodString
9-
props: z.ZodObject<Record<string, z.ZodTypeAny>, 'strip', z.ZodTypeAny>
10-
children: z.ZodArray<z.ZodLazy<any>, 'many'>
11-
}>
12-
13-
// eslint-disable-next-line @typescript-eslint/no-use-before-define
14-
const nodeArraySchema = z.lazy(() => nodeSchema)
15-
16-
const nodeSchema: NodeSchema = z.object({
17-
componentName: z.string().describe('The name of the component.'),
18-
props: z.object({}).describe('The props of the component.'),
19-
children: z.array(z.lazy(() => nodeArraySchema)).describe('The children of the component')
20-
})
21-
227
const inputSchema = z.object({
238
parentId: z
249
.string()
2510
.optional()
2611
.describe(
2712
'The id of the parent node. If not provided, the new node will be added to the root. if you don\'t know the parentId, you can use the tool "get_page_schema" to get the page schema. if you want to add to page root, just don\'t provide the parentId.'
2813
),
29-
newNodeData: z.lazy(() => nodeSchema).describe('The new node data.'),
14+
newNodeData: z.object({
15+
componentName: z.string().describe('The name of the component.'),
16+
props: z.record(z.string(), z.any()).describe('The props of the component.'),
17+
children: z
18+
.array(z.record(z.string(), z.any()))
19+
.describe('Array of child nodes; each child has the same shape as newNodeData (recursive tree).')
20+
}),
3021
position: z
3122
.enum(['before', 'after'])
3223
.optional()
@@ -60,11 +51,6 @@ export const addNode = {
6051
const { props = {}, children = [] } = newNodeData
6152

6253
const validateResult = validateParams(args, {
63-
componentName: {
64-
required: true,
65-
message:
66-
'Component name is required, if you don\'t know the component name, you can use the tool "get_component_list" to get the component detail.'
67-
},
6854
parentId: {
6955
validator: (value: string) => {
7056
const parentNode = useCanvas().getNodeById(value)
@@ -87,27 +73,55 @@ export const addNode = {
8773
return validateResult.error
8874
}
8975

76+
const { getMaterial } = useMaterial()
77+
const material = getMaterial(componentName)
78+
const isEmptyPlainObject =
79+
material &&
80+
typeof material === 'object' &&
81+
!Array.isArray(material) &&
82+
Object.keys(material as Record<string, unknown>).length === 0
83+
84+
if (!newNodeData.componentName || isEmptyPlainObject) {
85+
return {
86+
isError: true,
87+
content: [
88+
{
89+
type: 'text',
90+
text: JSON.stringify({
91+
status: 'error',
92+
errorCode: 'COMPONENT_NAME_REQUIRED',
93+
reason: 'Component name is required',
94+
userMessage: 'Component name is required. Fetch the available component list.',
95+
next_action: {
96+
type: 'tool_call',
97+
name: 'get_component_list',
98+
args: {}
99+
}
100+
})
101+
}
102+
]
103+
}
104+
}
105+
106+
const insertData = {
107+
componentName,
108+
props,
109+
children
110+
}
111+
90112
useCanvas().operateNode({
91113
type: 'insert',
92114
parentId: parentId!,
93115
// @ts-ignore
94-
newNodeData: {
95-
componentName,
96-
props,
97-
children
98-
},
116+
newNodeData: insertData,
99117
position: position!,
100118
referTargetNodeId
101119
})
102120

103121
const res = {
104122
status: 'success',
105123
message: `Node added successfully`,
106-
data: {
107-
componentName,
108-
props,
109-
children
110-
}
124+
data: insertData
111125
}
112126

113127
return {

packages/canvas/DesignCanvas/src/mcp/tools/changeNodeProps.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { z } from 'zod'
22
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
33

44
const inputSchema = z.object({
5-
id: z.string().describe('The id of the node to change the props of.'),
5+
id: z
6+
.string()
7+
.describe(
8+
'The id of the node to change the props of. if you don\'t know the id, you can use the tool "get_current_selected_node" to get the current selected node. or you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.'
9+
),
610
props: z
7-
.object({})
11+
.record(z.string(), z.any())
812
.describe(
913
'The props of the component. if you don\'t know available props, you can use the "get_component_detail" tool to get component detail and available props.'
1014
),
@@ -33,6 +37,37 @@ export const changeNodeProps = {
3337
props = {}
3438
}
3539

40+
const node = useCanvas().getNodeById(id)
41+
if (!node) {
42+
return {
43+
content: [
44+
{
45+
isError: true,
46+
type: 'text',
47+
text: JSON.stringify({
48+
errorCode: 'NODE_NOT_FOUND',
49+
reason: `Node not found: ${id}`,
50+
userMessage: `Node not found: ${id}. Fetch the available node list.`,
51+
next_action: [
52+
{
53+
type: 'tool_call',
54+
name: 'get_current_selected_node',
55+
args: {},
56+
when: 'you want to change the props of the current selected node'
57+
},
58+
{
59+
type: 'tool_call',
60+
name: 'get_page_schema',
61+
args: {},
62+
when: 'you want to change the props of the node with the specified id'
63+
}
64+
]
65+
})
66+
}
67+
]
68+
}
69+
}
70+
3671
useCanvas().operateNode({
3772
type: 'changeProps',
3873
id,

packages/canvas/DesignCanvas/src/mcp/tools/delNode.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { z } from 'zod'
22
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
33

44
const inputSchema = z.object({
5-
id: z.string().describe('The id of the node to delete.')
5+
id: z
6+
.string()
7+
.describe(
8+
'The id of the node to delete. if you don\'t know the id, you can use the tool "get_current_selected_node" to get the current selected node. or you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.'
9+
)
610
})
711

812
export const delNode = {

packages/canvas/DesignCanvas/src/mcp/tools/queryNodeById.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { z } from 'zod'
22
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
33

44
const inputSchema = z.object({
5-
id: z.string().describe('The id of the node to query.')
5+
id: z
6+
.string()
7+
.describe(
8+
'The id of the node to query. if you don\'t know the id, you can use the tool "get_current_selected_node" to get the current selected node. or you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.'
9+
)
610
})
711

812
export const queryNodeById = {
@@ -29,8 +33,23 @@ export const queryNodeById = {
2933
isError: true,
3034
type: 'text',
3135
text: JSON.stringify({
32-
status: 'error',
33-
message: 'Node not found, please check the id is correct.'
36+
errorCode: 'NODE_NOT_FOUND',
37+
reason: `Node not found: ${id}`,
38+
userMessage: `Node not found: ${id}. Fetch the available node list.`,
39+
next_action: [
40+
{
41+
type: 'tool_call',
42+
name: 'get_current_selected_node',
43+
args: {},
44+
when: 'you want to query the current selected node'
45+
},
46+
{
47+
type: 'tool_call',
48+
name: 'get_page_schema',
49+
args: {},
50+
when: 'you want to query the node with the specified id'
51+
}
52+
]
3453
})
3554
}
3655
]

packages/canvas/DesignCanvas/src/mcp/tools/selectSpecificNode.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { z } from 'zod'
22
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
33

44
const inputSchema = z.object({
5-
id: z.string().describe('The id of the node to select.')
5+
id: z
6+
.string()
7+
.describe(
8+
'The id of the node to select. if you don\'t know the id, you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.'
9+
)
610
})
711

812
export const selectSpecificNode = {
@@ -21,6 +25,36 @@ export const selectSpecificNode = {
2125
},
2226
callback: async (args: z.infer<typeof inputSchema>) => {
2327
const { id } = args
28+
const node = useCanvas().getNodeById(id)
29+
30+
if (!node) {
31+
return {
32+
content: [
33+
{
34+
type: 'text',
35+
text: JSON.stringify({
36+
errorCode: 'NODE_NOT_FOUND',
37+
reason: `Node not found: ${id}`,
38+
userMessage: `Node not found: ${id}. Fetch the available node list.`,
39+
next_action: [
40+
{
41+
type: 'tool_call',
42+
name: 'get_current_selected_node',
43+
args: {},
44+
when: 'you want to select the current selected node'
45+
},
46+
{
47+
type: 'tool_call',
48+
name: 'get_page_schema',
49+
args: {},
50+
when: 'you want to select the node with the specified id'
51+
}
52+
]
53+
})
54+
}
55+
]
56+
}
57+
}
2458

2559
useCanvas().canvasApi.value?.selectNode?.(id, 'clickTree')
2660

packages/layout/src/composable/useLayout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ export default () => {
503503

504504
const getAllPlugins = () => {
505505
return getAllMergeMeta()
506-
.filter((item) => item.type === 'plugin')
506+
.filter((item) => item.type === 'plugins')
507507
.map((item) => {
508508
return {
509509
id: item.id,

packages/layout/src/mcp/tools/switchPlugin.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,34 @@ export const switchPluginPanel = {
1414
inputSchema: inputSchema.shape,
1515
callback: async (_args: z.infer<typeof inputSchema>) => {
1616
const { pluginId, operation } = _args
17-
const { activePlugin, closePlugin } = useLayout()
17+
const { activePlugin, closePlugin, getAllPlugins } = useLayout()
18+
const plugins = await getAllPlugins()
19+
const plugin = plugins.find((item) => item.id === pluginId)
1820

19-
if (operation === 'open' && pluginId) {
21+
if (!plugin) {
22+
return {
23+
content: [
24+
{
25+
type: 'text',
26+
text: JSON.stringify({
27+
errorCode: 'PLUGIN_NOT_FOUND',
28+
reason: `Unknown pluginId: ${pluginId}`,
29+
userMessage: 'Plugin not found. Fetch the available plugin list.',
30+
next_action: {
31+
type: 'tool_call',
32+
name: 'get_all_plugins',
33+
args: {}
34+
}
35+
})
36+
}
37+
]
38+
}
39+
}
40+
41+
if (operation === 'open') {
2042
await activePlugin(pluginId)
2143
} else {
22-
await closePlugin()
44+
await closePlugin(true)
2345
}
2446

2547
const res = {

packages/plugins/page/src/composable/usePage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ const createNewPage = async ({
603603
} catch (error) {
604604
return {
605605
success: false,
606-
error: JSON.stringify(error?.message || error)
606+
error: JSON.stringify(error instanceof Error ? error.message : error)
607607
}
608608
}
609609
}
@@ -625,7 +625,7 @@ const deletePage = async (id) => {
625625
} catch (error) {
626626
return {
627627
success: false,
628-
error: JSON.stringify(error?.message || error)
628+
error: JSON.stringify(error instanceof Error ? error.message : error)
629629
}
630630
}
631631
}
@@ -648,7 +648,7 @@ const updatePageById = async (id, params) => {
648648
} catch (error) {
649649
return {
650650
success: false,
651-
error: JSON.stringify(error?.message || error)
651+
error: JSON.stringify(error instanceof Error ? error.message : error)
652652
}
653653
}
654654
}

packages/plugins/page/src/mcp/tools/addPage.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import { usePage } from '@opentiny/tiny-engine-meta-register'
33

44
const inputSchema = z.object({
55
name: z.string().describe('The name of the page. The name must be unique and Capitalize the first letter.'),
6-
route: z.string().describe('The route of the page'),
6+
route: z
7+
.string()
8+
.describe(
9+
'The route of the page. only allow contain english letter, number, underline, hyphen, slash, and start with english letter.'
10+
),
711
parentId: z
812
.string()
913
.optional()
1014
.describe(
11-
'The parent id of the page, if not provided, the page will be created at the root level. if provided, the page will be created at the specified parent id.'
15+
'The parent id of the page, if not provided, the page will be created at the root level. if provided, the page will be created at the specified parent id. if you don\'t know the parentId, you can use the tool "get_page_list" to get the page list.'
1216
)
1317
})
1418

@@ -23,14 +27,14 @@ export const addPage = {
2327
callback: async (args: z.infer<typeof inputSchema>) => {
2428
const { name, route, parentId } = args
2529
const { createNewPage } = usePage()
26-
const { success, data } = await createNewPage({ name, route, parentId })
30+
const { success, data, error } = await createNewPage({ name, route, parentId })
2731

2832
if (!success) {
2933
const res = {
3034
status: 'error',
3135
message: 'Failed to create page',
3236
data: {
33-
error: 'Failed to create page'
37+
error: error || 'Failed to create page'
3438
}
3539
}
3640

0 commit comments

Comments
 (0)