@opentiny/tiny-vue-mcp 0.0.5 → 0.0.6-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +293 -289
- package/dist/{index.d.ts → packages/mcp/index.d.ts} +2 -2
- package/dist/packages/mcp/src/utils/__tests__/webmcp.test.d.ts +1 -0
- package/dist/{src → packages/mcp/src}/utils/defineTool.d.ts +13 -2
- package/dist/packages/mcp/vitest.config.d.ts +2 -0
- package/dist/src/utils/defineTool.js +41 -4
- package/index.ts +9 -3
- package/package.json +11 -2
- package/src/utils/__tests__/webmcp.test.ts +153 -0
- package/src/utils/defineTool.ts +112 -41
- package/vite.config.ts +29 -29
- package/vitest.config.ts +8 -0
- /package/dist/{src → packages/mcp/src}/action-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/alert/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/amount/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/anchor/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/area/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/autocomplete/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/avatar/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/badge/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/base-select/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/breadcrumb/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/bulletin-board/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/button/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/button-group/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/calendar/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/calendar-view/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/card/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/card-group/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/card-template/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/carousel/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/cascader/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/cascader-panel/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/chart/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/checkbox/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/checkbox-button/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/checkbox-group/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/collapse/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/color-picker/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/color-select-panel/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/company/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/country/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/crop/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/currency/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/date-panel/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/date-picker/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/date-range/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/date-table/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dept/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dialog-box/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dialog-select/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/divider/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/drawer/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/drop-roles/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/drop-times/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dropdown/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dropdown-item/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dropdown-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/dynamic-scroller/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/empty/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/espace/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/exception/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/fall-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/file-upload/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/filter-panel/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/float-button/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/floatbar/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/flowchart/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/fluent-editor/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/footer/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/form/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/form-item/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/fullscreen/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/grid/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/grid-select/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/guide/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/header/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/hrapprover/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/image/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/image-viewer/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/infinite-scroll/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/input/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/ip-address/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/lang/en.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/lang/zh-CN.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/layout/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/link/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/link-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/loading/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/locales/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/logon-user/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/logout/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/message/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/message-box/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/milestone/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/mind-map/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/modal/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/month-table/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/nav-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/notification/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/notify/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/number-animation/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/numeric/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/option-group/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/pager/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/pagination/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/pop-upload/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/popconfirm/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/popeditor/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/popover/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/progress/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/qr-code/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/query-builder/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/radio/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/rate/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/recycle-scroller/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/result/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/rich-text/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/rich-text-editor/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/roles/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/scroll-bar/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/scroll-text/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/search/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/select/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/select-dropdown/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/select-wrapper/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/side/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/skeleton/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/slider/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/space/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/spin/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/split/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/statistic/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/steps/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/sticky/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/switch/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/table/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tabs/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tag/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tag-group/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/text-popup/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/time-line/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/time-panel/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/time-picker/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/time-range/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/time-select/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/time-spinner/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/toggle-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tooltip/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/transfer/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tree/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tree-menu/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/tree-select/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/upload/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/user/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/user-account/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/user-contact/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/user-head/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/user-link/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/utils/defineComponentTool.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/utils/locale.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/virtual-scroll-box/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/virtual-tree/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/watermark/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/wizard/index.d.ts +0 -0
- /package/dist/{src → packages/mcp/src}/year-table/index.d.ts +0 -0
|
@@ -1,7 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { z as f } from "zod";
|
|
2
|
+
import { zodToJsonSchema as g } from "zod-to-json-schema";
|
|
3
|
+
import { initializeBuiltinWebMCP as y } from "@opentiny/next-sdk/core";
|
|
4
|
+
const h = /* @__PURE__ */ new WeakSet(), S = () => {
|
|
5
|
+
if (typeof document > "u") return;
|
|
6
|
+
const t = document;
|
|
7
|
+
if (!h.has(document)) {
|
|
8
|
+
if (y(), !t.modelContext)
|
|
9
|
+
throw new Error("[TinyVue WebMCP] Failed to initialize document.modelContext");
|
|
10
|
+
h.add(document);
|
|
11
|
+
}
|
|
12
|
+
return t.modelContext;
|
|
13
|
+
}, w = ({ componentMcpConfig: t, instance: b, server: i, business: s }) => {
|
|
14
|
+
const o = `${s.id}_${t.name}`, c = `${s.description}-${t.description}`, n = t.paramsSchema || {}, a = (e) => t.cb(b, e);
|
|
15
|
+
if (i) {
|
|
16
|
+
const e = i.registerTool(o, { title: o, description: c, inputSchema: n }, a);
|
|
17
|
+
return () => e.remove();
|
|
18
|
+
}
|
|
19
|
+
const m = S();
|
|
20
|
+
if (!m) return;
|
|
21
|
+
const l = Object.keys(n).length > 0, d = f.object(n).strict(), u = l ? d.refine((e) => Object.keys(e).length > 0, {
|
|
22
|
+
message: "At least one component operation is required"
|
|
23
|
+
}) : d, r = new AbortController(), p = m.registerTool(
|
|
24
|
+
{
|
|
25
|
+
name: o,
|
|
26
|
+
title: o,
|
|
27
|
+
description: c,
|
|
28
|
+
// WebMCP 接收 JSON Schema,而非 MCP Server 接收的 Zod shape。
|
|
29
|
+
inputSchema: {
|
|
30
|
+
...g(u, { $refStrategy: "none" }),
|
|
31
|
+
...l ? { minProperties: 1 } : {}
|
|
32
|
+
},
|
|
33
|
+
execute: async (e) => a(await u.parseAsync(e))
|
|
34
|
+
},
|
|
35
|
+
{ signal: r.signal }
|
|
36
|
+
);
|
|
37
|
+
return Promise.resolve(p).catch((e) => {
|
|
38
|
+
r.signal.aborted || console.error(`[TinyVue WebMCP] Failed to register ${o}`, e);
|
|
39
|
+
}), () => r.abort();
|
|
4
40
|
};
|
|
5
41
|
export {
|
|
6
|
-
|
|
42
|
+
w as defineTool,
|
|
43
|
+
S as ensureWebMcp
|
|
7
44
|
};
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTool } from './src/utils/defineTool'
|
|
1
|
+
import { defineTool, ensureWebMcp } from './src/utils/defineTool'
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
3
|
import type { Business, ComponentMcpConfig } from './src/utils/defineTool'
|
|
4
4
|
import zhCN from './src/lang/zh-CN'
|
|
@@ -148,13 +148,19 @@ import { getWizardConfig } from './src/wizard'
|
|
|
148
148
|
|
|
149
149
|
export const createMcpTools = (
|
|
150
150
|
instance: any,
|
|
151
|
-
{ server, business }: { server
|
|
151
|
+
{ server, business }: { server?: McpServer; business: Business },
|
|
152
152
|
componentMcpConfig: ComponentMcpConfig
|
|
153
153
|
) => {
|
|
154
|
-
defineTool({ business, componentMcpConfig, instance, server })
|
|
154
|
+
return defineTool({ business, componentMcpConfig, instance, server })
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
export const getTinyVueMcpConfig = ({ t }: { t?: ((path: string) => string) | null } = {}) => {
|
|
158
|
+
try {
|
|
159
|
+
ensureWebMcp()
|
|
160
|
+
} catch {
|
|
161
|
+
// 配置也用于不依赖浏览器 WebMCP 的显式 Server 模式。
|
|
162
|
+
// 未传 server 时,工具注册会重试初始化并报告失败。
|
|
163
|
+
}
|
|
158
164
|
if (i18n && t) {
|
|
159
165
|
i18n.t = t
|
|
160
166
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentiny/tiny-vue-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6-alpha.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "ISC",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/opentiny/tiny-vue-mcp.git",
|
|
10
|
+
"directory": "packages/mcp"
|
|
11
|
+
},
|
|
7
12
|
"keywords": [],
|
|
8
13
|
"main": "dist/index.js",
|
|
9
14
|
"module": "dist/index.js",
|
|
@@ -11,14 +16,18 @@
|
|
|
11
16
|
"access": "public"
|
|
12
17
|
},
|
|
13
18
|
"dependencies": {
|
|
19
|
+
"@opentiny/next-sdk": "0.4.11",
|
|
14
20
|
"zod": "^3.25.23",
|
|
21
|
+
"zod-to-json-schema": "^3.25.2",
|
|
15
22
|
"@modelcontextprotocol/sdk": "1.23.0"
|
|
16
23
|
},
|
|
17
24
|
"devDependencies": {
|
|
25
|
+
"vitest": "^3.2.7",
|
|
18
26
|
"vite": "^6.3.1",
|
|
19
27
|
"vite-plugin-dts": "^4.5.4"
|
|
20
28
|
},
|
|
21
29
|
"scripts": {
|
|
22
|
-
"build": "vite build"
|
|
30
|
+
"build": "vite build",
|
|
31
|
+
"test": "vitest run"
|
|
23
32
|
}
|
|
24
33
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { initializeBuiltinWebMCP } from '@opentiny/next-sdk/core'
|
|
4
|
+
import { defineTool } from '../defineTool'
|
|
5
|
+
import { createMcpTools, getTinyVueMcpConfig } from '../../../index'
|
|
6
|
+
|
|
7
|
+
vi.mock('@opentiny/next-sdk/core', () => ({ initializeBuiltinWebMCP: vi.fn() }))
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.mocked(initializeBuiltinWebMCP).mockReset()
|
|
10
|
+
})
|
|
11
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
12
|
+
|
|
13
|
+
const business = { id: 'test-grid', description: 'Test grid' }
|
|
14
|
+
const makeConfig = () => ({
|
|
15
|
+
name: 'grid_component_tools',
|
|
16
|
+
description: 'Grid operations',
|
|
17
|
+
paramsSchema: { getTableData: z.boolean().optional() },
|
|
18
|
+
cb: vi.fn(async () => ({ content: [{ type: 'text', text: 'rows' }] }))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('WebMCP component tool adapter', () => {
|
|
22
|
+
it('registers a descriptor with JSON Schema and awaits validated execution', async () => {
|
|
23
|
+
const registerTool = vi.fn()
|
|
24
|
+
vi.stubGlobal('document', { modelContext: { registerTool } })
|
|
25
|
+
const componentMcpConfig = makeConfig()
|
|
26
|
+
const instance = { rows: [] }
|
|
27
|
+
const dispose = defineTool({ instance, business, componentMcpConfig })
|
|
28
|
+
const [tool, options] = registerTool.mock.calls[0]
|
|
29
|
+
|
|
30
|
+
expect(tool.name).toBe('test-grid_grid_component_tools')
|
|
31
|
+
expect(tool.inputSchema).toMatchObject({
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: { getTableData: { type: 'boolean' } },
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
minProperties: 1
|
|
36
|
+
})
|
|
37
|
+
expect(await tool.execute({ getTableData: true })).toEqual({ content: [{ type: 'text', text: 'rows' }] })
|
|
38
|
+
expect(componentMcpConfig.cb).toHaveBeenCalledWith(instance, { getTableData: true })
|
|
39
|
+
dispose?.()
|
|
40
|
+
expect(options.signal.aborted).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects unknown operations, invalid types and empty arguments before invoking the component', async () => {
|
|
44
|
+
const registerTool = vi.fn()
|
|
45
|
+
vi.stubGlobal('document', { modelContext: { registerTool } })
|
|
46
|
+
const componentMcpConfig = makeConfig()
|
|
47
|
+
defineTool({ instance: {}, business, componentMcpConfig })
|
|
48
|
+
const [tool] = registerTool.mock.calls[0]
|
|
49
|
+
await expect(tool.execute({ missing: true })).rejects.toThrow()
|
|
50
|
+
await expect(tool.execute({ getTableData: 'true' })).rejects.toThrow()
|
|
51
|
+
await expect(tool.execute({})).rejects.toThrow()
|
|
52
|
+
expect(componentMcpConfig.cb).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it.each([undefined, {}])(
|
|
56
|
+
'allows empty arguments for parameterless tools with paramsSchema %j',
|
|
57
|
+
async (paramsSchema) => {
|
|
58
|
+
const registerTool = vi.fn()
|
|
59
|
+
vi.stubGlobal('document', { modelContext: { registerTool } })
|
|
60
|
+
const componentMcpConfig = { ...makeConfig(), paramsSchema }
|
|
61
|
+
const instance = {}
|
|
62
|
+
defineTool({ instance, business, componentMcpConfig })
|
|
63
|
+
const [tool] = registerTool.mock.calls[0]
|
|
64
|
+
|
|
65
|
+
expect(tool.inputSchema).not.toHaveProperty('minProperties')
|
|
66
|
+
expect(tool.inputSchema).toMatchObject({ type: 'object', properties: {}, additionalProperties: false })
|
|
67
|
+
expect(await tool.execute({})).toEqual({ content: [{ type: 'text', text: 'rows' }] })
|
|
68
|
+
expect(componentMcpConfig.cb).toHaveBeenCalledWith(instance, {})
|
|
69
|
+
await expect(tool.execute({ unexpected: true })).rejects.toThrow()
|
|
70
|
+
expect(componentMcpConfig.cb).toHaveBeenCalledOnce()
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
it('does not initialize browser APIs during SSR', () => {
|
|
75
|
+
vi.stubGlobal('document', undefined)
|
|
76
|
+
expect(defineTool({ instance: {}, business, componentMcpConfig: makeConfig() })).toBeUndefined()
|
|
77
|
+
expect(initializeBuiltinWebMCP).not.toHaveBeenCalled()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('injects the Polyfill before registration and only initializes once per document', () => {
|
|
81
|
+
const registerTool = vi.fn()
|
|
82
|
+
const browserDocument = { modelContext: undefined as unknown }
|
|
83
|
+
vi.stubGlobal('document', browserDocument)
|
|
84
|
+
vi.mocked(initializeBuiltinWebMCP).mockImplementation(() => {
|
|
85
|
+
browserDocument.modelContext = { registerTool }
|
|
86
|
+
})
|
|
87
|
+
expect(getTinyVueMcpConfig().components.Grid).toBeDefined()
|
|
88
|
+
defineTool({ instance: {}, business, componentMcpConfig: makeConfig() })
|
|
89
|
+
defineTool({ instance: {}, business: { ...business, id: 'second-grid' }, componentMcpConfig: makeConfig() })
|
|
90
|
+
expect(initializeBuiltinWebMCP).toHaveBeenCalledOnce()
|
|
91
|
+
expect(registerTool).toHaveBeenCalledTimes(2)
|
|
92
|
+
expect(vi.mocked(initializeBuiltinWebMCP).mock.invocationCallOrder[0]).toBeLessThan(
|
|
93
|
+
registerTool.mock.invocationCallOrder[0]
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('reports initialization failure and allows a later retry', () => {
|
|
98
|
+
const browserDocument = { modelContext: undefined as unknown }
|
|
99
|
+
vi.stubGlobal('document', browserDocument)
|
|
100
|
+
expect(() => defineTool({ instance: {}, business, componentMcpConfig: makeConfig() })).toThrow(
|
|
101
|
+
'Failed to initialize'
|
|
102
|
+
)
|
|
103
|
+
vi.mocked(initializeBuiltinWebMCP).mockImplementation(() => {
|
|
104
|
+
browserDocument.modelContext = { registerTool: vi.fn() }
|
|
105
|
+
})
|
|
106
|
+
expect(() => defineTool({ instance: {}, business, componentMcpConfig: makeConfig() })).not.toThrow()
|
|
107
|
+
expect(initializeBuiltinWebMCP).toHaveBeenCalledTimes(2)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it.each(['missing-context', 'throws'])('keeps explicit-server setup working when initialization %s', (failure) => {
|
|
111
|
+
vi.stubGlobal('document', {})
|
|
112
|
+
if (failure === 'throws') {
|
|
113
|
+
vi.mocked(initializeBuiltinWebMCP).mockImplementation(() => {
|
|
114
|
+
throw new Error('Polyfill unavailable')
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
const config = getTinyVueMcpConfig()
|
|
118
|
+
const remove = vi.fn()
|
|
119
|
+
const registerTool = vi.fn(() => ({ remove }))
|
|
120
|
+
const server = { registerTool } as unknown as NonNullable<Parameters<typeof defineTool>[0]['server']>
|
|
121
|
+
const dispose = createMcpTools({}, { business, server }, config.components.Grid)
|
|
122
|
+
|
|
123
|
+
expect(registerTool).toHaveBeenCalledWith(
|
|
124
|
+
'test-grid_grid_component_tools',
|
|
125
|
+
expect.objectContaining({ inputSchema: config.components.Grid.paramsSchema }),
|
|
126
|
+
expect.any(Function)
|
|
127
|
+
)
|
|
128
|
+
expect(initializeBuiltinWebMCP).toHaveBeenCalledOnce()
|
|
129
|
+
dispose?.()
|
|
130
|
+
expect(remove).toHaveBeenCalledOnce()
|
|
131
|
+
// 配置获取允许回退,真正选择 WebMCP 注册时仍重试并报告初始化错误。
|
|
132
|
+
expect(() => createMcpTools({}, { business }, config.components.Grid)).toThrow(
|
|
133
|
+
failure === 'throws' ? 'Polyfill unavailable' : 'Failed to initialize'
|
|
134
|
+
)
|
|
135
|
+
expect(initializeBuiltinWebMCP).toHaveBeenCalledTimes(2)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('preserves explicit legacy MCP Server registration and removes its tool on disposal', () => {
|
|
139
|
+
const remove = vi.fn()
|
|
140
|
+
const registerTool = vi.fn(() => ({ remove }))
|
|
141
|
+
const server = { registerTool } as unknown as NonNullable<Parameters<typeof defineTool>[0]['server']>
|
|
142
|
+
const componentMcpConfig = makeConfig()
|
|
143
|
+
const dispose = defineTool({ instance: {}, business, componentMcpConfig, server })
|
|
144
|
+
expect(registerTool).toHaveBeenCalledWith(
|
|
145
|
+
'test-grid_grid_component_tools',
|
|
146
|
+
expect.objectContaining({ inputSchema: componentMcpConfig.paramsSchema }),
|
|
147
|
+
expect.any(Function)
|
|
148
|
+
)
|
|
149
|
+
dispose?.()
|
|
150
|
+
expect(remove).toHaveBeenCalledOnce()
|
|
151
|
+
expect(initializeBuiltinWebMCP).not.toHaveBeenCalled()
|
|
152
|
+
})
|
|
153
|
+
})
|
package/src/utils/defineTool.ts
CHANGED
|
@@ -1,41 +1,112 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
4
|
+
import { initializeBuiltinWebMCP } from '@opentiny/next-sdk/core'
|
|
5
|
+
|
|
6
|
+
const initializedDocuments = new WeakSet<Document>()
|
|
7
|
+
|
|
8
|
+
// 每个页面只初始化一次,避免后续组件注册时重置已有工具或重复安装桥接。
|
|
9
|
+
// SSR 不触碰浏览器 API;初始化失败时允许下次重试。
|
|
10
|
+
export const ensureWebMcp = () => {
|
|
11
|
+
if (typeof document === 'undefined') return
|
|
12
|
+
const currentDocument = document as WebMcpDocument
|
|
13
|
+
if (!initializedDocuments.has(document)) {
|
|
14
|
+
initializeBuiltinWebMCP()
|
|
15
|
+
if (!currentDocument.modelContext) {
|
|
16
|
+
throw new Error('[TinyVue WebMCP] Failed to initialize document.modelContext')
|
|
17
|
+
}
|
|
18
|
+
initializedDocuments.add(document)
|
|
19
|
+
}
|
|
20
|
+
return currentDocument.modelContext
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 开发人员传入的业务信息 id要保证唯一性 */
|
|
24
|
+
export interface Business {
|
|
25
|
+
id: string
|
|
26
|
+
description: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 组件所有的功能行为都集中在tool里面 */
|
|
30
|
+
export interface ComponentMcpConfig {
|
|
31
|
+
name: string
|
|
32
|
+
description: string
|
|
33
|
+
paramsSchema?: Record<string, any>
|
|
34
|
+
cb: (instance: any, args: Record<string, any>) => any
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DefineToolOptions {
|
|
38
|
+
componentMcpConfig: ComponentMcpConfig
|
|
39
|
+
/** tool 可操纵的上下文的变量和方法集合 */
|
|
40
|
+
instance: any
|
|
41
|
+
server?: McpServer
|
|
42
|
+
business: Business
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 快速定义一个业务tool的方法
|
|
47
|
+
* @param componentMcpConfig 组件的可用工具集
|
|
48
|
+
* @param instance 工具可操作的上下文实例, Vue中是传入实例,其它场景可能传入 {数据, 方法等}
|
|
49
|
+
* @param server MCP 服务器实例
|
|
50
|
+
* @param business 业务信息
|
|
51
|
+
*/
|
|
52
|
+
export const defineTool = ({ componentMcpConfig, instance, server, business }: DefineToolOptions) => {
|
|
53
|
+
const name = `${business.id}_${componentMcpConfig.name}`
|
|
54
|
+
const description = `${business.description}-${componentMcpConfig.description}`
|
|
55
|
+
const paramsSchema = componentMcpConfig.paramsSchema || {}
|
|
56
|
+
const cb = (args: Record<string, any>) => {
|
|
57
|
+
return componentMcpConfig.cb(instance, args)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 保留显式传入 MCP Server 的旧调用方式。
|
|
61
|
+
if (server) {
|
|
62
|
+
const tool = server.registerTool(name, { title: name, description, inputSchema: paramsSchema }, cb)
|
|
63
|
+
return () => tool.remove()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// WebMCP 注册由组件库内部完成,应用仅需提供业务信息。
|
|
67
|
+
const modelContext = ensureWebMcp()
|
|
68
|
+
if (!modelContext) return
|
|
69
|
+
|
|
70
|
+
const hasOperations = Object.keys(paramsSchema).length > 0
|
|
71
|
+
const objectSchema = z.object(paramsSchema).strict()
|
|
72
|
+
const schema = hasOperations
|
|
73
|
+
? objectSchema.refine((args) => Object.keys(args).length > 0, {
|
|
74
|
+
message: 'At least one component operation is required'
|
|
75
|
+
})
|
|
76
|
+
: objectSchema
|
|
77
|
+
const controller = new AbortController()
|
|
78
|
+
const registration = modelContext.registerTool(
|
|
79
|
+
{
|
|
80
|
+
name,
|
|
81
|
+
title: name,
|
|
82
|
+
description,
|
|
83
|
+
// WebMCP 接收 JSON Schema,而非 MCP Server 接收的 Zod shape。
|
|
84
|
+
inputSchema: {
|
|
85
|
+
...zodToJsonSchema(schema, { $refStrategy: 'none' }),
|
|
86
|
+
...(hasOperations ? { minProperties: 1 } : {})
|
|
87
|
+
},
|
|
88
|
+
execute: async (args: Record<string, unknown>) => cb(await schema.parseAsync(args))
|
|
89
|
+
},
|
|
90
|
+
{ signal: controller.signal }
|
|
91
|
+
)
|
|
92
|
+
// 原生/Polyfill 可以异步注册;卸载导致的主动取消不作为异常上报。
|
|
93
|
+
Promise.resolve(registration).catch((error: unknown) => {
|
|
94
|
+
if (!controller.signal.aborted) console.error(`[TinyVue WebMCP] Failed to register ${name}`, error)
|
|
95
|
+
})
|
|
96
|
+
return () => controller.abort()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface WebMcpDocument extends Document {
|
|
100
|
+
modelContext?: {
|
|
101
|
+
registerTool: (
|
|
102
|
+
tool: {
|
|
103
|
+
name: string
|
|
104
|
+
title: string
|
|
105
|
+
description: string
|
|
106
|
+
inputSchema: Record<string, unknown>
|
|
107
|
+
execute: (args: Record<string, unknown>) => Promise<unknown>
|
|
108
|
+
},
|
|
109
|
+
options: { signal: AbortSignal }
|
|
110
|
+
) => void | Promise<void>
|
|
111
|
+
}
|
|
112
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { defineConfig } from 'vite'
|
|
2
|
-
import dts from 'vite-plugin-dts'
|
|
3
|
-
|
|
4
|
-
// https://vite.dev/config/
|
|
5
|
-
export default defineConfig(() => {
|
|
6
|
-
return {
|
|
7
|
-
plugins: [
|
|
8
|
-
// 生成类型声明文件
|
|
9
|
-
dts()
|
|
10
|
-
],
|
|
11
|
-
// 库模式配置
|
|
12
|
-
build: {
|
|
13
|
-
lib: {
|
|
14
|
-
entry: './index.ts'
|
|
15
|
-
},
|
|
16
|
-
rollupOptions: {
|
|
17
|
-
external: ['zod'],
|
|
18
|
-
input: ['index.ts'],
|
|
19
|
-
output: [
|
|
20
|
-
{
|
|
21
|
-
format: 'es',
|
|
22
|
-
entryFileNames: '[name].js',
|
|
23
|
-
preserveModules: true
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
})
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import dts from 'vite-plugin-dts'
|
|
3
|
+
|
|
4
|
+
// https://vite.dev/config/
|
|
5
|
+
export default defineConfig(() => {
|
|
6
|
+
return {
|
|
7
|
+
plugins: [
|
|
8
|
+
// 生成类型声明文件
|
|
9
|
+
dts()
|
|
10
|
+
],
|
|
11
|
+
// 库模式配置
|
|
12
|
+
build: {
|
|
13
|
+
lib: {
|
|
14
|
+
entry: './index.ts'
|
|
15
|
+
},
|
|
16
|
+
rollupOptions: {
|
|
17
|
+
external: ['zod', 'zod-to-json-schema', '@opentiny/next-sdk/core'],
|
|
18
|
+
input: ['index.ts'],
|
|
19
|
+
output: [
|
|
20
|
+
{
|
|
21
|
+
format: 'es',
|
|
22
|
+
entryFileNames: '[name].js',
|
|
23
|
+
preserveModules: true
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|
package/vitest.config.ts
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|