Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,604 changes: 861 additions & 743 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions template/tinyvue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"@babel/core": "^7.25.2",
"@gaonengwww/mock-server": "^1.0.5",
"@opentiny/icons": "^0.1.3",
"@opentiny/next-remoter": "^0.0.10",
"@opentiny/next-sdk": "^0.1.15",
"@opentiny/next-remoter": "0.2.7",
"@opentiny/next-sdk": "0.2.7",
"@opentiny/vue": "^3.28.0",
"@opentiny/vue-huicharts": "~3.28.0",
"@opentiny/vue-icon": "~3.28.0",
Expand Down
77 changes: 15 additions & 62 deletions template/tinyvue/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts" setup>
import { TinyRemoter } from '@opentiny/next-remoter'
import { createMessageChannelPairTransport, WebMcpClient, WebMcpServer, z } from '@opentiny/next-sdk'
import { TinyConfigProvider } from '@opentiny/vue'
import TinyThemeTool from '@opentiny/vue-theme/theme-tool'
import { onMounted, provide, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { onMounted } from 'vue'
import GlobalSetting from '@/components/global-setting/index.vue'
import { useTheme } from './hooks/useTheme'
import { clientTransport, createMcpServer } from './mcp-servers'
import { skills } from './skills'
import '@opentiny/next-remoter/dist/style.css'

const theme = new TinyThemeTool()
Expand All @@ -25,51 +25,18 @@ const design = {
},
}

const sessionId = ref('')
const [serverTransport, clientTransport] = createMessageChannelPairTransport()
provide('serverTransport', serverTransport)

const AGENT_URL = 'https://agent.opentiny.design/api/v1/webmcp-trial/'
// 将本地 MCP Server 注册到 TinyRemoter
// key 为服务器名称(自定义),type: 'local' 表示浏览器本地运行
const mcpServers = {
'my-mcp-server': {
type: 'local',
transport: clientTransport,
},
}

// 启动 MCP Server(注册工具 + 建立通信通道)
onMounted(async () => {
const server = new WebMcpServer()
const $router = useRouter()
const $route = useRoute()

// TODO: 参数需要优化,用户不会知道具体的路由路径,用户的语言可能是:帮我打开菜单管理页面,这时应该根据名称获取路由路径,再做路由跳转
// 进一步优化:用户可能在任意页面直接提需求:帮我创建 xx 菜单,这时 AI 应该先跳转菜单管理页面,然后调佣创建菜单的工具
server.registerTool(
'switch-router',
{
title: '切换路由',
description: '切换路由',
inputSchema: {
routerPath: z.string().describe('路由路径'),
},
},
async ({ routerPath }) => {
if ($route.path === routerPath) {
return { content: [{ type: 'text', text: routerPath }] }
}

$router.push(import.meta.env.VITE_CONTEXT + routerPath)
return { content: [{ type: 'text', text: routerPath }] }
},
)
await server.connect(serverTransport)

// 创建 WebMcpClient ,并与 WebAgent 连接
const client = new WebMcpClient()
await client.connect(clientTransport)
const { sessionId: sessionID } = await client.connect({
agent: true,

// sessionId 为可选参数。若传入该参数,系统将使用指定值作为会话标识;若未传入,WebAgent 服务将自动生成一个随机的字符串作为 sessionId。为便于通过 MCP Inspector 工具进行调试,此处采用了固定的 sessionId。用户亦可通过浏览器原生提供的 crypto.randomUUID() 方法生成随机字符串作为会话标识。
sessionId: 'd299a869-c674-4125-a84b-bb4e24079b99',

url: `${AGENT_URL}mcp`,
})
sessionId.value = sessionID
await createMcpServer()
Comment on lines +37 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Vue 3 Composition API, are child components mounted before the parent component's onMounted()hook runs, and is the initial render committed before an asynconMounted() callback finishes?

💡 Result:

Yes.

  • Child components mount before the parent’s onMounted() runs (for synchronous children): Vue considers a component “mounted” only after all of its synchronous child components have been mounted and the component’s DOM has been created/inserted, so the parent’s mounted / onMounted necessarily happens after those children. [1]

  • The initial render is already committed before an async onMounted(async () => …) finishes: onMounted runs after the component has finished the initial rendering and created the DOM nodes, and Vue does not delay/“await” client rendering on a Promise returned from mounted/onMounted. (Vue only has an explicit “wait for the Promise” lifecycle hook for SSR: serverPrefetch.) [2] [3]

Sources: [1] Vue Options API lifecycle: mounted semantics. [2] Vue Guide: onMounted runs after initial render + DOM creation. [3] Vue Options API lifecycle: serverPrefetch is awaited by the server renderer.


Gate TinyRemoter on successful MCP startup.

createMcpServer() is async and runs in onMounted(), but TinyRemoter is rendered unconditionally during initial render—which happens before onMounted runs. If bootstrap is still in flight or throws, the remoter is exposed to an uninitialized local server with an unhandled error.

🐛 Proposed fix
-import { onMounted } from 'vue'
+import { onMounted, ref } from 'vue'
@@
 const mcpServers = {
   'my-mcp-server': {
     type: 'local',
     transport: clientTransport,
   },
 }
 
+const mcpReady = ref(false)
+
 // 启动 MCP Server(注册工具 + 建立通信通道)
 onMounted(async () => {
-  await createMcpServer()
+  try {
+    await createMcpServer()
+    mcpReady.value = true
+  } catch (error) {
+    console.error('Failed to start local MCP server', error)
+  }
 })
@@
   <TinyRemoter
+    v-if="mcpReady"
     :skills="skills"
     :mcp-servers="mcpServers"
   />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 启动 MCP Server(注册工具 + 建立通信通道)
onMounted(async () => {
const server = new WebMcpServer()
const $router = useRouter()
const $route = useRoute()
// TODO: 参数需要优化,用户不会知道具体的路由路径,用户的语言可能是:帮我打开菜单管理页面,这时应该根据名称获取路由路径,再做路由跳转
// 进一步优化:用户可能在任意页面直接提需求:帮我创建 xx 菜单,这时 AI 应该先跳转菜单管理页面,然后调佣创建菜单的工具
server.registerTool(
'switch-router',
{
title: '切换路由',
description: '切换路由',
inputSchema: {
routerPath: z.string().describe('路由路径'),
},
},
async ({ routerPath }) => {
if ($route.path === routerPath) {
return { content: [{ type: 'text', text: routerPath }] }
}
$router.push(import.meta.env.VITE_CONTEXT + routerPath)
return { content: [{ type: 'text', text: routerPath }] }
},
)
await server.connect(serverTransport)
// 创建 WebMcpClient ,并与 WebAgent 连接
const client = new WebMcpClient()
await client.connect(clientTransport)
const { sessionId: sessionID } = await client.connect({
agent: true,
// sessionId 为可选参数。若传入该参数,系统将使用指定值作为会话标识;若未传入,WebAgent 服务将自动生成一个随机的字符串作为 sessionId。为便于通过 MCP Inspector 工具进行调试,此处采用了固定的 sessionId。用户亦可通过浏览器原生提供的 crypto.randomUUID() 方法生成随机字符串作为会话标识。
sessionId: 'd299a869-c674-4125-a84b-bb4e24079b99',
url: `${AGENT_URL}mcp`,
})
sessionId.value = sessionID
await createMcpServer()
// 启动 MCP Server(注册工具 + 建立通信通道)
onMounted(async () => {
try {
await createMcpServer()
mcpReady.value = true
} catch (error) {
console.error('Failed to start local MCP server', error)
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/tinyvue/src/App.vue` around lines 37 - 39, The TinyRemoter component
is rendered before createMcpServer() completes in onMounted(), exposing it to an
uninitialized server; modify the component to track a ready state (e.g.,
mcpReady) updated after await createMcpServer() (and set false on error) and
conditionally render/gate TinyRemoter based on that flag, handling bootstrap
errors by not mounting TinyRemoter when createMcpServer() throws; refer to
createMcpServer, onMounted and TinyRemoter to locate where to add the boolean
state and conditional rendering.

})
</script>

Expand All @@ -82,22 +49,8 @@ onMounted(async () => {
<GlobalSetting />
</div>
<TinyRemoter
:agent-root="AGENT_URL"
:session-id="sessionId"
:menu-items="[
{
action: 'qr-code',
show: false,
},
{
action: 'remote-control',
show: false,
},
{
action: 'remote-url',
show: false,
},
]"
:skills="skills"
:mcp-servers="mcpServers"
/>
</template>

Expand Down
4 changes: 4 additions & 0 deletions template/tinyvue/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import TinySearchBox from '@opentiny/vue-search-box'
import { createApp } from 'vue'
import { setNavigator } from '@opentiny/next-sdk'
import globalComponents from '@/components'
import App from './App.vue'
import directive from './directive'
Expand All @@ -22,3 +23,6 @@ app.use(directive)
app.use(TinySearchBox)

app.mount('#app')

// 必须在 router 注册后调用,让 SDK 持有 router.push 的引用
setNavigator((route) => router.push(route))
25 changes: 25 additions & 0 deletions template/tinyvue/src/mcp-servers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createMessageChannelPairTransport, WebMcpServer, withPageTools } from '@opentiny/next-sdk'
import registerLocaleManagementTools from './locale/tools'
import registerMenuManagementTools from './menu/tools'
import registerPermissionManagementTools from './permission/tools'
import registerRoleManagementTools from './role/tools'
import registerUserManagementTools from './user/tools'

const rawServer = new WebMcpServer()
const [serverTransport, clientTransport] = createMessageChannelPairTransport()

// withPageTools 包装后,registerTool 第三个参数支持路由配置对象
export const server = withPageTools(rawServer)

// clientTransport 导出给 TinyRemoter 使用
export { clientTransport }

export async function createMcpServer() {
registerLocaleManagementTools(server)
registerUserManagementTools(server)
registerRoleManagementTools(server)
registerPermissionManagementTools(server)
registerMenuManagementTools(server)
// 最后建立连接,确保所有工具已注册完毕
await rawServer.connect(serverTransport)
}
Comment on lines +17 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In @opentiny/next-sdk0.2.6-beta.0, isWebMcpServer.connect()safe to call multiple times on the same instance, and doesregisterTool() allow duplicate registrations of the same tool name?

💡 Result:

  • WebMcpServer.connect() (in @opentiny/next-sdk v0.2.6-beta.0) is not designed to be safely called multiple times on the same instance. The implementation always assigns this.transport and then calls await this.server.connect(this.transport) with no “already connected” guard or automatic close()/disconnect first—so a second call will attempt to connect again (behavior then depends on the underlying @modelcontextprotocol/sdk server, and may error or lead to inconsistent state). [1]

  • registerTool() does not allow two tools with the same name to coexist. In this SDK it simply delegates to this.server.registerTool(name, ...) (so the tool name is the identity key). [1] The MCP TS SDK docs also describe registerTool() as a way tools can be “added … or updated at runtime” (and that it triggers tool-list-changed notifications), implying re-registering the same name is treated as an update/replace, not a duplicate entry. [2]

Sources: [1] [2]


Make createMcpServer() idempotent.

This exported initializer has side effects on every call: it re-registers tools and reconnects the same server/transport pair. Calling connect() multiple times on the same WebMcpServer instance is unsafe and can cause errors or inconsistent state. A remount or HMR path could trigger this repeatedly.

Proposed fix
+let mcpServerReady: Promise<void> | undefined
+
 export async function createMcpServer() {
-  registerLocaleManagementTools(server)
-  // 最后建立连接,确保所有工具已注册完毕
-  await rawServer.connect(serverTransport)
+  if (!mcpServerReady) {
+    mcpServerReady = (async () => {
+      registerLocaleManagementTools(server)
+      // 最后建立连接,确保所有工具已注册完毕
+      await rawServer.connect(serverTransport)
+    })()
+  }
+
+  await mcpServerReady
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function createMcpServer() {
registerLocaleManagementTools(server)
// 最后建立连接,确保所有工具已注册完毕
await rawServer.connect(serverTransport)
}
let mcpServerReady: Promise<void> | undefined
export async function createMcpServer() {
if (!mcpServerReady) {
mcpServerReady = (async () => {
registerLocaleManagementTools(server)
// 最后建立连接,确保所有工具已注册完毕
await rawServer.connect(serverTransport)
})()
}
await mcpServerReady
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/tinyvue/src/mcp-servers/index.ts` around lines 13 - 17,
createMcpServer currently re-registers tools and calls
rawServer.connect(serverTransport) on every invocation; make it idempotent by
adding an initialization guard and a safe connect check: introduce a
module-level boolean (e.g., mcpServerInitialized) and return early if true, call
registerLocaleManagementTools only when first initializing, and before calling
rawServer.connect(serverTransport) check whether the WebMcpServer is already
connected (or wrap connect call in try/catch and set the initialized flag only
after a successful connection); update createMcpServer to set the flag after
successful registration/connection so repeated calls are no-ops.

22 changes: 22 additions & 0 deletions template/tinyvue/src/mcp-servers/locale/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { PageAwareServer } from '@opentiny/next-sdk'
import { z } from '@opentiny/next-sdk'

function registerLocaleManagementTools(server: PageAwareServer) {
server.registerTool(
'add-i18n-entry',
{
title: '添加国际化词条',
description: '添加国际化词条',
inputSchema: {
key: z.string().describe('词条关键字,请自行创建,不要询问用户'),
content: z.string().describe('词条内容'),
lang: z.union([z.literal(1), z.literal(2)]).describe('词条语言ID,英文 enUS 为:1,中文 zhCN 为:2'),
},
},
// 第三个参数传路由配置:工具被调用时自动跳转到 /locale
// 页面加载完成后,通过 postMessage 把 input 转发给页面内的处理器
{ route: '/vue-pro/locale' },
)
}

export default registerLocaleManagementTools
24 changes: 24 additions & 0 deletions template/tinyvue/src/mcp-servers/menu/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { PageAwareServer } from '@opentiny/next-sdk'
import { z } from '@opentiny/next-sdk'

function registerMenuManagementTools(server: PageAwareServer) {
server.registerTool(
'add-menu',
{
title: '添加菜单',
description: '添加菜单',
inputSchema: {
name: z.string().describe('名称'),
order: z.number().describe('优先级').default(0),
parentMenu: z.string().describe('父菜单').optional(),
icon: z.string().describe('图标').optional().default(''),
component: z.string().describe('组件'),
path: z.string().describe('路径'),
locale: z.string().describe('国际化'),
},
},
{ route: '/vue-pro/menu/allMenu' },
)
}

export default registerMenuManagementTools
19 changes: 19 additions & 0 deletions template/tinyvue/src/mcp-servers/permission/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PageAwareServer } from '@opentiny/next-sdk'
import { z } from '@opentiny/next-sdk'

function registerPermissionManagementTools(server: PageAwareServer) {
server.registerTool(
'add-permission',
{
title: '添加权限',
description: '添加权限',
inputSchema: {
name: z.string().describe('权限名称'),
desc: z.string().describe('权限描述'),
},
},
{ route: '/vue-pro/permission/allPermission' },
)
}

export default registerPermissionManagementTools
33 changes: 33 additions & 0 deletions template/tinyvue/src/mcp-servers/role/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { PageAwareServer } from '@opentiny/next-sdk'
import { z } from '@opentiny/next-sdk'

function registerRoleManagementTools(server: PageAwareServer) {
server.registerTool(
'add-role',
{
title: '添加角色',
description: '添加角色,不需要生成角色卡片',
inputSchema: {
name: z.string().describe('角色名称'),
// TODO: 用户的语言可能是添加用户和删除用户的权限,而不是 user::add 和 user::remove 权限或者权限 ID 为 2 和 3,需要做下转换
permissions: z.array(z.number()).describe('角色拥有的权限'),
},
},
{ route: '/vue-pro/role/allRole' },
)

server.registerTool(
'bind-menu-for-role',
{
title: '绑定菜单',
description: '给某个角色绑定菜单',
inputSchema: {
role: z.string().describe('需要绑定菜单的角色名称'),
menu: z.string().describe('需要绑定的菜单名称'),
},
},
{ route: '/vue-pro/role/allRole' },
)
}

export default registerRoleManagementTools
29 changes: 29 additions & 0 deletions template/tinyvue/src/mcp-servers/user/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { PageAwareServer } from '@opentiny/next-sdk'
import { z } from '@opentiny/next-sdk'

function registerUserManagementTools(server: PageAwareServer) {
server.registerTool(
'add-user',
{
title: '添加用户',
description: '添加用户,可选参数不需要用户提供,也不用创建表单卡片,直接根据用户提供的信息添加用户即可',
inputSchema: {
email: z.string().describe('邮箱'),
password: z.string().describe('密码'),
name: z.string().describe('用户名'),
address: z.string().describe('地址').optional(),
department: z.string().describe('所属部门').optional(),
roleIds: z.array(z.number()).describe('职位').optional(),
employeeType: z.string().describe('招聘类型').optional(),
probationDate: z.array(z.date()).describe('试用期起止时间').optional(),
probationDuration: z.string().describe('试用期时长').optional(),
protocolStart: z.date().describe('劳动合同开始日期').optional(),
protocolEnd: z.date().describe('劳动合同结束日期').optional(),
Comment on lines +18 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Zod z.date() validate ISO date strings or only Date objects?

💡 Result:

z.date() validates only JavaScript Date instances—it will fail on ISO date/datetime strings. For example, "2022-01-12T06:15:00.000Z" does not pass z.date(). [1]

If you need to accept ISO strings, use a string schema like z.iso.date() (for YYYY-MM-DD) / z.iso.datetime() (for datetimes), or coerce into a Date with z.coerce.date() (then validate as a Date). [1]


🏁 Script executed:

cd template/tinyvue && find . -name "tools.ts" | grep user

Repository: opentiny/tiny-pro

Length of output: 92


🏁 Script executed:

cat -n template/tinyvue/src/mcp-servers/user/tools.ts | head -100

Repository: opentiny/tiny-pro

Length of output: 1375


🏁 Script executed:

# Search for any date coercion or preprocessing related to this schema
rg -i "coerce|date" template/tinyvue/src/mcp-servers/user/tools.ts -A 2 -B 2

Repository: opentiny/tiny-pro

Length of output: 518


Replace z.date() with z.coerce.date() for date fields.

The schema uses z.date() on lines 18, 20, and 21, which validates only JavaScript Date instances and will reject ISO date strings (e.g., "2022-01-12T06:15:00.000Z"). Since dates from the message transport arrive as ISO strings, validation will fail at runtime. Use z.coerce.date() instead to accept ISO strings and automatically coerce them to Date objects:

Suggested fix
- probationDate: z.array(z.date()).describe('试用期起止时间').optional(),
+ probationDate: z.array(z.coerce.date()).describe('试用期起止时间').optional(),
- protocolStart: z.date().describe('劳动合同开始日期').optional(),
+ protocolStart: z.coerce.date().describe('劳动合同开始日期').optional(),
- protocolEnd: z.date().describe('劳动合同结束日期').optional(),
+ protocolEnd: z.coerce.date().describe('劳动合同结束日期').optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
probationDate: z.array(z.date()).describe('试用期起止时间').optional(),
probationDuration: z.string().describe('试用期时长').optional(),
protocolStart: z.date().describe('劳动合同开始日期').optional(),
protocolEnd: z.date().describe('劳动合同结束日期').optional(),
probationDate: z.array(z.coerce.date()).describe('试用期起止时间').optional(),
probationDuration: z.string().describe('试用期时长').optional(),
protocolStart: z.coerce.date().describe('劳动合同开始日期').optional(),
protocolEnd: z.coerce.date().describe('劳动合同结束日期').optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@template/tinyvue/src/mcp-servers/user/tools.ts` around lines 18 - 21, The
date fields in the schema (probationDate, protocolStart, protocolEnd) are using
z.date(), which rejects ISO date strings; update those validators to
z.coerce.date() (and for the array use z.array(z.coerce.date())) so incoming ISO
string dates are coerced to Date objects at validation time; keep
probationDuration as string. Locate the symbols probationDate,
probationDuration, protocolStart, protocolEnd and replace z.date() with
z.coerce.date() accordingly.

status: z.string().describe('状态').optional(),
},
},
{ route: '/vue-pro/userManager/allInfo' },
)
}

export default registerUserManagementTools
5 changes: 5 additions & 0 deletions template/tinyvue/src/skills/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const skills = import.meta.glob('./**/*', {
query: '?raw', // 以原始文本形式导入,不经过模块解析
import: 'default', // 取模块的 default 导出(即文件内容字符串)
eager: true // 同步加载,避免异步等待
}) as Record<string, string>
36 changes: 36 additions & 0 deletions template/tinyvue/src/skills/tiny-pro-operator/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: tiny-pro-operator
description: TinyPro 项目的前端页面操作指南。当用户需要新建页面、配置菜单、设置权限(v-permission)、国际化(i18n)管理或进行 TinyPro 相关页面的操作时触发。该技能提供从菜单管理、词条管理、用户管理、权限管理的完整指引。
---

# TinyPro 操作指南 (tiny-pro-operator)

本技能旨在帮助开发者在 TinyPro 框架下高效地完成页面操作,比如:菜单管理、权限管理和国际化配置。

以下是系统管理包含的功能,每个功能都有对应的路由和 MCP 工具。

- 菜单管理(路由:`menu/allMenu`):
- ✅️创建菜单 `add-menu`
- ❌修改菜单
- ❌删除菜单
- 权限管理(路由:`permission/allPermission`):
- ✅️添加权限 `add-permission`
- ❌编辑权限
- ❌删除权限
- 角色管理(路由:`role/allRole`):
- ✅️添加角色 `add-role`
- ❌编辑角色(名称、权限)
- ❌删除角色
- ✅️绑定目录 bind-menu-for-role
- 用户管理(路由:`userManager/allInfo`):
- ✅️添加用户 `add-user`
- ❌编辑用户
- ❌修改密码
- ❌删除用户(包含批量删除用户)
- 国际化词条管理(路由:`locale`):
- ✅️添加词条 `add-i18n-entry`
- ❌删除词条(包含批量删除词条)

当用户询问相关操作时,需要跳转到对应的路由,调用对应的 MCP 工具。

例如:“帮我添加权限:good::add,描述是:创建商品”,则需要调用 MCP 工具 `add-permission`。
Loading