@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.
Files changed (167) hide show
  1. package/dist/index.js +293 -289
  2. package/dist/{index.d.ts → packages/mcp/index.d.ts} +2 -2
  3. package/dist/packages/mcp/src/utils/__tests__/webmcp.test.d.ts +1 -0
  4. package/dist/{src → packages/mcp/src}/utils/defineTool.d.ts +13 -2
  5. package/dist/packages/mcp/vitest.config.d.ts +2 -0
  6. package/dist/src/utils/defineTool.js +41 -4
  7. package/index.ts +9 -3
  8. package/package.json +11 -2
  9. package/src/utils/__tests__/webmcp.test.ts +153 -0
  10. package/src/utils/defineTool.ts +112 -41
  11. package/vite.config.ts +29 -29
  12. package/vitest.config.ts +8 -0
  13. /package/dist/{src → packages/mcp/src}/action-menu/index.d.ts +0 -0
  14. /package/dist/{src → packages/mcp/src}/alert/index.d.ts +0 -0
  15. /package/dist/{src → packages/mcp/src}/amount/index.d.ts +0 -0
  16. /package/dist/{src → packages/mcp/src}/anchor/index.d.ts +0 -0
  17. /package/dist/{src → packages/mcp/src}/area/index.d.ts +0 -0
  18. /package/dist/{src → packages/mcp/src}/autocomplete/index.d.ts +0 -0
  19. /package/dist/{src → packages/mcp/src}/avatar/index.d.ts +0 -0
  20. /package/dist/{src → packages/mcp/src}/badge/index.d.ts +0 -0
  21. /package/dist/{src → packages/mcp/src}/base-select/index.d.ts +0 -0
  22. /package/dist/{src → packages/mcp/src}/breadcrumb/index.d.ts +0 -0
  23. /package/dist/{src → packages/mcp/src}/bulletin-board/index.d.ts +0 -0
  24. /package/dist/{src → packages/mcp/src}/button/index.d.ts +0 -0
  25. /package/dist/{src → packages/mcp/src}/button-group/index.d.ts +0 -0
  26. /package/dist/{src → packages/mcp/src}/calendar/index.d.ts +0 -0
  27. /package/dist/{src → packages/mcp/src}/calendar-view/index.d.ts +0 -0
  28. /package/dist/{src → packages/mcp/src}/card/index.d.ts +0 -0
  29. /package/dist/{src → packages/mcp/src}/card-group/index.d.ts +0 -0
  30. /package/dist/{src → packages/mcp/src}/card-template/index.d.ts +0 -0
  31. /package/dist/{src → packages/mcp/src}/carousel/index.d.ts +0 -0
  32. /package/dist/{src → packages/mcp/src}/cascader/index.d.ts +0 -0
  33. /package/dist/{src → packages/mcp/src}/cascader-panel/index.d.ts +0 -0
  34. /package/dist/{src → packages/mcp/src}/chart/index.d.ts +0 -0
  35. /package/dist/{src → packages/mcp/src}/checkbox/index.d.ts +0 -0
  36. /package/dist/{src → packages/mcp/src}/checkbox-button/index.d.ts +0 -0
  37. /package/dist/{src → packages/mcp/src}/checkbox-group/index.d.ts +0 -0
  38. /package/dist/{src → packages/mcp/src}/collapse/index.d.ts +0 -0
  39. /package/dist/{src → packages/mcp/src}/color-picker/index.d.ts +0 -0
  40. /package/dist/{src → packages/mcp/src}/color-select-panel/index.d.ts +0 -0
  41. /package/dist/{src → packages/mcp/src}/company/index.d.ts +0 -0
  42. /package/dist/{src → packages/mcp/src}/country/index.d.ts +0 -0
  43. /package/dist/{src → packages/mcp/src}/crop/index.d.ts +0 -0
  44. /package/dist/{src → packages/mcp/src}/currency/index.d.ts +0 -0
  45. /package/dist/{src → packages/mcp/src}/date-panel/index.d.ts +0 -0
  46. /package/dist/{src → packages/mcp/src}/date-picker/index.d.ts +0 -0
  47. /package/dist/{src → packages/mcp/src}/date-range/index.d.ts +0 -0
  48. /package/dist/{src → packages/mcp/src}/date-table/index.d.ts +0 -0
  49. /package/dist/{src → packages/mcp/src}/dept/index.d.ts +0 -0
  50. /package/dist/{src → packages/mcp/src}/dialog-box/index.d.ts +0 -0
  51. /package/dist/{src → packages/mcp/src}/dialog-select/index.d.ts +0 -0
  52. /package/dist/{src → packages/mcp/src}/divider/index.d.ts +0 -0
  53. /package/dist/{src → packages/mcp/src}/drawer/index.d.ts +0 -0
  54. /package/dist/{src → packages/mcp/src}/drop-roles/index.d.ts +0 -0
  55. /package/dist/{src → packages/mcp/src}/drop-times/index.d.ts +0 -0
  56. /package/dist/{src → packages/mcp/src}/dropdown/index.d.ts +0 -0
  57. /package/dist/{src → packages/mcp/src}/dropdown-item/index.d.ts +0 -0
  58. /package/dist/{src → packages/mcp/src}/dropdown-menu/index.d.ts +0 -0
  59. /package/dist/{src → packages/mcp/src}/dynamic-scroller/index.d.ts +0 -0
  60. /package/dist/{src → packages/mcp/src}/empty/index.d.ts +0 -0
  61. /package/dist/{src → packages/mcp/src}/espace/index.d.ts +0 -0
  62. /package/dist/{src → packages/mcp/src}/exception/index.d.ts +0 -0
  63. /package/dist/{src → packages/mcp/src}/fall-menu/index.d.ts +0 -0
  64. /package/dist/{src → packages/mcp/src}/file-upload/index.d.ts +0 -0
  65. /package/dist/{src → packages/mcp/src}/filter-panel/index.d.ts +0 -0
  66. /package/dist/{src → packages/mcp/src}/float-button/index.d.ts +0 -0
  67. /package/dist/{src → packages/mcp/src}/floatbar/index.d.ts +0 -0
  68. /package/dist/{src → packages/mcp/src}/flowchart/index.d.ts +0 -0
  69. /package/dist/{src → packages/mcp/src}/fluent-editor/index.d.ts +0 -0
  70. /package/dist/{src → packages/mcp/src}/footer/index.d.ts +0 -0
  71. /package/dist/{src → packages/mcp/src}/form/index.d.ts +0 -0
  72. /package/dist/{src → packages/mcp/src}/form-item/index.d.ts +0 -0
  73. /package/dist/{src → packages/mcp/src}/fullscreen/index.d.ts +0 -0
  74. /package/dist/{src → packages/mcp/src}/grid/index.d.ts +0 -0
  75. /package/dist/{src → packages/mcp/src}/grid-select/index.d.ts +0 -0
  76. /package/dist/{src → packages/mcp/src}/guide/index.d.ts +0 -0
  77. /package/dist/{src → packages/mcp/src}/header/index.d.ts +0 -0
  78. /package/dist/{src → packages/mcp/src}/hrapprover/index.d.ts +0 -0
  79. /package/dist/{src → packages/mcp/src}/image/index.d.ts +0 -0
  80. /package/dist/{src → packages/mcp/src}/image-viewer/index.d.ts +0 -0
  81. /package/dist/{src → packages/mcp/src}/infinite-scroll/index.d.ts +0 -0
  82. /package/dist/{src → packages/mcp/src}/input/index.d.ts +0 -0
  83. /package/dist/{src → packages/mcp/src}/ip-address/index.d.ts +0 -0
  84. /package/dist/{src → packages/mcp/src}/lang/en.d.ts +0 -0
  85. /package/dist/{src → packages/mcp/src}/lang/zh-CN.d.ts +0 -0
  86. /package/dist/{src → packages/mcp/src}/layout/index.d.ts +0 -0
  87. /package/dist/{src → packages/mcp/src}/link/index.d.ts +0 -0
  88. /package/dist/{src → packages/mcp/src}/link-menu/index.d.ts +0 -0
  89. /package/dist/{src → packages/mcp/src}/loading/index.d.ts +0 -0
  90. /package/dist/{src → packages/mcp/src}/locales/index.d.ts +0 -0
  91. /package/dist/{src → packages/mcp/src}/logon-user/index.d.ts +0 -0
  92. /package/dist/{src → packages/mcp/src}/logout/index.d.ts +0 -0
  93. /package/dist/{src → packages/mcp/src}/menu/index.d.ts +0 -0
  94. /package/dist/{src → packages/mcp/src}/message/index.d.ts +0 -0
  95. /package/dist/{src → packages/mcp/src}/message-box/index.d.ts +0 -0
  96. /package/dist/{src → packages/mcp/src}/milestone/index.d.ts +0 -0
  97. /package/dist/{src → packages/mcp/src}/mind-map/index.d.ts +0 -0
  98. /package/dist/{src → packages/mcp/src}/modal/index.d.ts +0 -0
  99. /package/dist/{src → packages/mcp/src}/month-table/index.d.ts +0 -0
  100. /package/dist/{src → packages/mcp/src}/nav-menu/index.d.ts +0 -0
  101. /package/dist/{src → packages/mcp/src}/notification/index.d.ts +0 -0
  102. /package/dist/{src → packages/mcp/src}/notify/index.d.ts +0 -0
  103. /package/dist/{src → packages/mcp/src}/number-animation/index.d.ts +0 -0
  104. /package/dist/{src → packages/mcp/src}/numeric/index.d.ts +0 -0
  105. /package/dist/{src → packages/mcp/src}/option-group/index.d.ts +0 -0
  106. /package/dist/{src → packages/mcp/src}/pager/index.d.ts +0 -0
  107. /package/dist/{src → packages/mcp/src}/pagination/index.d.ts +0 -0
  108. /package/dist/{src → packages/mcp/src}/pop-upload/index.d.ts +0 -0
  109. /package/dist/{src → packages/mcp/src}/popconfirm/index.d.ts +0 -0
  110. /package/dist/{src → packages/mcp/src}/popeditor/index.d.ts +0 -0
  111. /package/dist/{src → packages/mcp/src}/popover/index.d.ts +0 -0
  112. /package/dist/{src → packages/mcp/src}/progress/index.d.ts +0 -0
  113. /package/dist/{src → packages/mcp/src}/qr-code/index.d.ts +0 -0
  114. /package/dist/{src → packages/mcp/src}/query-builder/index.d.ts +0 -0
  115. /package/dist/{src → packages/mcp/src}/radio/index.d.ts +0 -0
  116. /package/dist/{src → packages/mcp/src}/rate/index.d.ts +0 -0
  117. /package/dist/{src → packages/mcp/src}/recycle-scroller/index.d.ts +0 -0
  118. /package/dist/{src → packages/mcp/src}/result/index.d.ts +0 -0
  119. /package/dist/{src → packages/mcp/src}/rich-text/index.d.ts +0 -0
  120. /package/dist/{src → packages/mcp/src}/rich-text-editor/index.d.ts +0 -0
  121. /package/dist/{src → packages/mcp/src}/roles/index.d.ts +0 -0
  122. /package/dist/{src → packages/mcp/src}/scroll-bar/index.d.ts +0 -0
  123. /package/dist/{src → packages/mcp/src}/scroll-text/index.d.ts +0 -0
  124. /package/dist/{src → packages/mcp/src}/search/index.d.ts +0 -0
  125. /package/dist/{src → packages/mcp/src}/select/index.d.ts +0 -0
  126. /package/dist/{src → packages/mcp/src}/select-dropdown/index.d.ts +0 -0
  127. /package/dist/{src → packages/mcp/src}/select-wrapper/index.d.ts +0 -0
  128. /package/dist/{src → packages/mcp/src}/side/index.d.ts +0 -0
  129. /package/dist/{src → packages/mcp/src}/skeleton/index.d.ts +0 -0
  130. /package/dist/{src → packages/mcp/src}/slider/index.d.ts +0 -0
  131. /package/dist/{src → packages/mcp/src}/space/index.d.ts +0 -0
  132. /package/dist/{src → packages/mcp/src}/spin/index.d.ts +0 -0
  133. /package/dist/{src → packages/mcp/src}/split/index.d.ts +0 -0
  134. /package/dist/{src → packages/mcp/src}/statistic/index.d.ts +0 -0
  135. /package/dist/{src → packages/mcp/src}/steps/index.d.ts +0 -0
  136. /package/dist/{src → packages/mcp/src}/sticky/index.d.ts +0 -0
  137. /package/dist/{src → packages/mcp/src}/switch/index.d.ts +0 -0
  138. /package/dist/{src → packages/mcp/src}/table/index.d.ts +0 -0
  139. /package/dist/{src → packages/mcp/src}/tabs/index.d.ts +0 -0
  140. /package/dist/{src → packages/mcp/src}/tag/index.d.ts +0 -0
  141. /package/dist/{src → packages/mcp/src}/tag-group/index.d.ts +0 -0
  142. /package/dist/{src → packages/mcp/src}/text-popup/index.d.ts +0 -0
  143. /package/dist/{src → packages/mcp/src}/time-line/index.d.ts +0 -0
  144. /package/dist/{src → packages/mcp/src}/time-panel/index.d.ts +0 -0
  145. /package/dist/{src → packages/mcp/src}/time-picker/index.d.ts +0 -0
  146. /package/dist/{src → packages/mcp/src}/time-range/index.d.ts +0 -0
  147. /package/dist/{src → packages/mcp/src}/time-select/index.d.ts +0 -0
  148. /package/dist/{src → packages/mcp/src}/time-spinner/index.d.ts +0 -0
  149. /package/dist/{src → packages/mcp/src}/toggle-menu/index.d.ts +0 -0
  150. /package/dist/{src → packages/mcp/src}/tooltip/index.d.ts +0 -0
  151. /package/dist/{src → packages/mcp/src}/transfer/index.d.ts +0 -0
  152. /package/dist/{src → packages/mcp/src}/tree/index.d.ts +0 -0
  153. /package/dist/{src → packages/mcp/src}/tree-menu/index.d.ts +0 -0
  154. /package/dist/{src → packages/mcp/src}/tree-select/index.d.ts +0 -0
  155. /package/dist/{src → packages/mcp/src}/upload/index.d.ts +0 -0
  156. /package/dist/{src → packages/mcp/src}/user/index.d.ts +0 -0
  157. /package/dist/{src → packages/mcp/src}/user-account/index.d.ts +0 -0
  158. /package/dist/{src → packages/mcp/src}/user-contact/index.d.ts +0 -0
  159. /package/dist/{src → packages/mcp/src}/user-head/index.d.ts +0 -0
  160. /package/dist/{src → packages/mcp/src}/user-link/index.d.ts +0 -0
  161. /package/dist/{src → packages/mcp/src}/utils/defineComponentTool.d.ts +0 -0
  162. /package/dist/{src → packages/mcp/src}/utils/locale.d.ts +0 -0
  163. /package/dist/{src → packages/mcp/src}/virtual-scroll-box/index.d.ts +0 -0
  164. /package/dist/{src → packages/mcp/src}/virtual-tree/index.d.ts +0 -0
  165. /package/dist/{src → packages/mcp/src}/watermark/index.d.ts +0 -0
  166. /package/dist/{src → packages/mcp/src}/wizard/index.d.ts +0 -0
  167. /package/dist/{src → packages/mcp/src}/year-table/index.d.ts +0 -0
@@ -1,7 +1,44 @@
1
- const o = ({ componentMcpConfig: e, instance: a, server: s, business: t }) => {
2
- const r = `${t.id}_${e.name}`, c = `${t.description}-${e.description}`, i = e.paramsSchema || {}, d = (m) => e.cb(a, m);
3
- s.registerTool(r, { title: r, description: c, inputSchema: i }, d);
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
- o as defineTool
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: McpServer; business: Business },
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.5",
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
+ })
@@ -1,41 +1,112 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
-
3
- /** 开发人员传入的业务信息 id要保证唯一性 */
4
- export interface Business {
5
- id: string
6
- description: string
7
- }
8
-
9
- /** 组件所有的功能行为都集中在tool里面 */
10
- export interface ComponentMcpConfig {
11
- name: string
12
- description: string
13
- paramsSchema?: Record<string, any>
14
- cb: (instance: any, args: Record<string, any>) => any
15
- }
16
-
17
- export interface DefineToolOptions {
18
- componentMcpConfig: ComponentMcpConfig
19
- /** tool 可操纵的上下文的变量和方法集合 */
20
- instance: any
21
- server: McpServer
22
- business: Business
23
- }
24
-
25
- /**
26
- * 快速定义一个业务tool的方法
27
- * @param componentMcpConfig 组件的可用工具集
28
- * @param instance 工具可操作的上下文实例, Vue中是传入实例,其它场景可能传入 {数据, 方法等}
29
- * @param server MCP 服务器实例
30
- * @param business 业务信息
31
- */
32
- export const defineTool = ({ componentMcpConfig, instance, server, business }: DefineToolOptions) => {
33
- const name = `${business.id}_${componentMcpConfig.name}`
34
- const description = `${business.description}-${componentMcpConfig.description}`
35
- const paramsSchema = componentMcpConfig.paramsSchema || {}
36
- const cb = (args: Record<string, any>) => {
37
- return componentMcpConfig.cb(instance, args)
38
- }
39
-
40
- server.registerTool(name, { title: name, description, inputSchema: paramsSchema }, cb)
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
+ })
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['src/utils/__tests__/*.test.ts']
7
+ }
8
+ })
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