@nebutra/mcp 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -1,10 +1,22 @@
1
1
  # @nebutra/mcp
2
2
 
3
- Status: **WIP**
3
+ Model Context Protocol primitives for Nebutra agent and tool surfaces.
4
4
 
5
- > **Status: WIP** — Not yet integrated into any production app. Do not import until this notice is removed.
5
+ This package provides a registry, client, middleware, consent store, debug
6
+ helpers, and internal-server registration seams around the official
7
+ `@modelcontextprotocol/sdk` runtime.
6
8
 
7
- Model Context Protocol (MCP) for AI agent tool calling.
9
+ ## Status: WIP
10
+
11
+ Experimental public package. The package is published for reuse and contribution
12
+ intake, but its package metadata still marks these production gaps explicitly:
13
+
14
+ - no production app integrations yet
15
+ - no production MCP transports beyond HTTP/local seams
16
+ - no durable remote tool discovery or app-level integration layer
17
+
18
+ Use it for local tools, integration prototypes, and package development. Keep
19
+ production deployments behind the application-level gateway and approval model.
8
20
 
9
21
  ## Installation
10
22
 
@@ -12,114 +24,92 @@ Model Context Protocol (MCP) for AI agent tool calling.
12
24
  pnpm add @nebutra/mcp
13
25
  ```
14
26
 
15
- ## Features
16
-
17
- - **MCP Client** — Execute tools across registered servers
18
- - **Server Registry** — Manage internal and external MCP servers
19
- - **Middleware** — Rate limiting, audit logging, access control
20
- - **Plan-based Access** — Tool access based on subscription tier
27
+ ## Exports
21
28
 
22
- ## Architecture
23
-
24
- ```
25
- Agent → MCPClient → ServerRegistry → MCP Servers
26
- ↓
27
- Middleware (rate-limit, audit, access-control)
28
- ```
29
+ | Path | Description |
30
+ | --- | --- |
31
+ | `@nebutra/mcp` | Main registry, client, host, consent, debug, middleware, server, and type exports |
32
+ | `@nebutra/mcp/client` | `MCPClient` and the shared `mcpClient` instance |
33
+ | `@nebutra/mcp/server` | Internal server registration helpers |
34
+ | `@nebutra/mcp/middleware` | Rate-limit, audit, access-control, and middleware composition helpers |
29
35
 
30
36
  ## Usage
31
37
 
32
- ### Initialize
33
-
34
- ```typescript
35
- import { mcpClient, registerInternalServers } from "@nebutra/mcp";
36
-
37
- // Register built-in servers
38
- registerInternalServers();
39
- ```
40
-
41
- ### Execute Tools
42
-
43
- ```typescript
44
- const result = await mcpClient.executeTool(
45
- "generate_text",
46
- { prompt: "Write a product description" },
47
- {
48
- requestId: "req-123",
49
- tenantId: "org_456",
50
- plan: "PRO",
51
- },
52
- );
53
- ```
54
-
55
- ### Available Tools
56
-
57
- | Tool | Plan | Description |
58
- | ----------------- | ---------- | ------------------- |
59
- | `generate_text` | FREE | AI text generation |
60
- | `translate` | FREE | AI translation |
61
- | `embed_text` | FREE | Generate embeddings |
62
- | `recommend_items` | PRO | Get recommendations |
63
- | `sync_products` | PRO | E-commerce sync |
64
- | `mint_nft` | ENTERPRISE | Web3 minting |
65
-
66
- ## Plan-based Access Control
67
-
68
- ```typescript
69
- const plans = {
70
- FREE: ["generate_text", "translate", "embed_text", "content_*"],
71
- PRO: ["*_recommend_*", "*_ecommerce_*", ...FREE],
72
- ENTERPRISE: ["*_web3_*", ...PRO],
73
- };
74
- ```
75
-
76
- ## Register Custom Server
38
+ ### Register a local server
77
39
 
78
- ```typescript
79
- import { serverRegistry } from "@nebutra/mcp";
40
+ ```ts
41
+ import { MCPClient, serverRegistry } from "@nebutra/mcp";
80
42
 
81
43
  serverRegistry.register({
82
- name: "custom-tools",
83
- url: "http://localhost:3001/mcp",
44
+ id: "workspace",
45
+ name: "Workspace tools",
46
+ description: "Local workspace utility tools",
47
+ endpoint: "local",
48
+ transport: "local",
84
49
  tools: [
85
50
  {
86
- name: "custom_tool",
87
- description: "My custom tool",
88
- parameters: { type: "object", properties: {} },
51
+ name: "echo",
52
+ description: "Return the provided message",
53
+ parameters: {
54
+ message: { type: "string", required: true },
55
+ },
56
+ allowedPlans: ["PRO", "ENTERPRISE"],
89
57
  },
90
58
  ],
59
+ handlers: {
60
+ echo: (args) => ({ message: args.message }),
61
+ },
91
62
  allowedPlans: ["PRO", "ENTERPRISE"],
92
63
  });
64
+
65
+ const client = new MCPClient(serverRegistry);
66
+ const result = await client.executeTool(
67
+ "workspace:echo",
68
+ { message: "hello" },
69
+ {
70
+ requestId: "req_123",
71
+ tenantId: "org_123",
72
+ userId: "user_123",
73
+ plan: "PRO",
74
+ },
75
+ );
93
76
  ```
94
77
 
95
- ## Middleware
78
+ ### Register built-in servers
96
79
 
97
- ### Rate Limiting
80
+ ```ts
81
+ import { getInternalServerIds, registerInternalServers } from "@nebutra/mcp";
98
82
 
99
- ```typescript
100
- // Configured per tool
101
- const limits = {
102
- generate_text: { requests: 100, window: "1m" },
103
- embed_text: { requests: 1000, window: "1h" },
104
- };
83
+ registerInternalServers();
84
+ const internalServerIds = getInternalServerIds();
105
85
  ```
106
86
 
107
- ### Audit Logging
87
+ ### Compose middleware
88
+
89
+ ```ts
90
+ import {
91
+ composeMCPMiddleware,
92
+ createAccessControlMiddleware,
93
+ createAuditMiddleware,
94
+ createRateLimitMiddleware,
95
+ } from "@nebutra/mcp/middleware";
96
+
97
+ const middleware = composeMCPMiddleware([
98
+ createRateLimitMiddleware({ maxRequests: 100, windowMs: 60_000 }),
99
+ createAccessControlMiddleware({ blockedTools: [] }),
100
+ createAuditMiddleware({ onLog: (entry) => auditSink.write(entry) }),
101
+ ]);
102
+ ```
108
103
 
109
- All tool executions are logged:
104
+ ## Runtime Notes
110
105
 
111
- ```json
112
- {
113
- "timestamp": "2024-01-15T10:30:00Z",
114
- "tool": "generate_text",
115
- "tenantId": "org_456",
116
- "userId": "user_123",
117
- "duration": 245,
118
- "success": true
119
- }
120
- ```
106
+ - HTTP execution uses `StreamableHTTPClientTransport` from the official MCP SDK.
107
+ - Local execution resolves registered handlers directly.
108
+ - WebSocket execution is not implemented in this package.
109
+ - `stdio` execution is rejected for browser/serverless contexts.
110
+ - Tool access is checked at both server and tool level for plan, tenant, and
111
+ permission gates.
121
112
 
122
- ## Related
113
+ ## License
123
114
 
124
- - [AI service](../../backends/python/ai/)
125
- - [API Gateway](../../backends/gateway/)
115
+ MIT
@@ -1,3 +1,7 @@
1
+ // src/client/mcpClient.ts
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+
1
5
  // src/registry/serverRegistry.ts
2
6
  var MCPServerRegistry = class {
3
7
  servers = /* @__PURE__ */ new Map();
@@ -134,7 +138,10 @@ var MCPClient = class {
134
138
  constructor(registry = serverRegistry) {
135
139
  this.registry = registry;
136
140
  }
141
+ registry;
137
142
  requestCounter = 0;
143
+ /** One SDK client per HTTP server id, reused across calls. */
144
+ httpClients = /* @__PURE__ */ new Map();
138
145
  /**
139
146
  * Execute a tool by name
140
147
  */
@@ -189,11 +196,23 @@ var MCPClient = class {
189
196
  }
190
197
  }
191
198
  /**
192
- * Execute via HTTP transport
199
+ * Execute via HTTP transport, using the official MCP SDK's
200
+ * StreamableHTTPClientTransport + Client to perform the `tools/call`.
193
201
  */
194
202
  async executeHttp(server, toolName, args, requestId) {
203
+ const client = await this.getHttpClient(server, requestId);
204
+ const shortName = toolName.startsWith(`${server.id}:`) ? toolName.slice(server.id.length + 1) : toolName;
205
+ return client.callTool({ name: shortName, arguments: args });
206
+ }
207
+ /**
208
+ * Lazily create and cache an SDK Client connected over StreamableHTTP for a
209
+ * given server. Authentication headers are derived from the same env vars
210
+ * the previous hand-written transport used.
211
+ */
212
+ async getHttpClient(server, requestId) {
213
+ const cached = this.httpClients.get(server.id);
214
+ if (cached) return cached;
195
215
  const headers = {
196
- "Content-Type": "application/json",
197
216
  "X-Request-ID": requestId
198
217
  };
199
218
  if (server.authentication?.type === "bearer") {
@@ -208,27 +227,20 @@ var MCPClient = class {
208
227
  headers[headerName] = apiKey;
209
228
  }
210
229
  }
211
- const response = await fetch(server.endpoint, {
212
- method: "POST",
213
- headers,
214
- body: JSON.stringify({
215
- jsonrpc: "2.0",
216
- id: requestId,
217
- method: "tools/call",
218
- params: {
219
- name: toolName,
220
- arguments: args
221
- }
222
- })
230
+ const transport = new StreamableHTTPClientTransport(new URL(server.endpoint), {
231
+ requestInit: { headers }
223
232
  });
224
- if (!response.ok) {
225
- throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
226
- }
227
- const data = await response.json();
228
- if (data.error) {
229
- throw new Error(data.error.message || "Unknown MCP error");
230
- }
231
- return data.result;
233
+ const client = new Client({ name: "nebutra-mcp-client", version: "0.1.0" });
234
+ await client.connect(transport);
235
+ this.httpClients.set(server.id, client);
236
+ return client;
237
+ }
238
+ /**
239
+ * Close all cached SDK HTTP clients and clear the cache.
240
+ */
241
+ async close() {
242
+ await Promise.all(Array.from(this.httpClients.values()).map((client) => client.close()));
243
+ this.httpClients.clear();
232
244
  }
233
245
  /**
234
246
  * Execute via WebSocket transport
@@ -311,9 +323,9 @@ async function readToolDebug(limit = 10) {
311
323
  }
312
324
 
313
325
  // src/host.ts
314
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
326
+ import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
315
327
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
316
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
328
+ import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
317
329
  import { CapabilityError } from "@nebutra/errors";
318
330
  function shortToolName(serverId, toolName) {
319
331
  return toolName.startsWith(`${serverId}:`) ? toolName.slice(serverId.length + 1) : toolName;
@@ -368,7 +380,7 @@ var McpHost = class {
368
380
  await this.#connectSdkClient(options, transport, `stdio://${command}`);
369
381
  }
370
382
  async connectStreamableHttp(endpoint, options) {
371
- const transport = new StreamableHTTPClientTransport(
383
+ const transport = new StreamableHTTPClientTransport2(
372
384
  new URL(endpoint),
373
385
  options.headers !== void 0 ? { requestInit: { headers: options.headers } } : {}
374
386
  );
@@ -468,7 +480,7 @@ var McpHost = class {
468
480
  }
469
481
  async #connectSdkClient(options, transport, endpoint) {
470
482
  const manifest = requireManifest(options.id, options.manifest);
471
- const client = new Client({ name: "nebutra-tool-protocol", version: "0.1.0" });
483
+ const client = new Client2({ name: "nebutra-tool-protocol", version: "0.1.0" });
472
484
  await client.connect(transport);
473
485
  const listed = await client.listTools();
474
486
  const tools = listed.tools.map(
@@ -606,8 +618,160 @@ function composeMCPMiddleware(middlewares) {
606
618
  };
607
619
  }
608
620
 
621
+ // src/server/uiAgentServer.ts
622
+ import {
623
+ loadAgentComponentContract,
624
+ loadAgentManifest,
625
+ searchAgentComponents,
626
+ validateAgentComponent
627
+ } from "@nebutra/ui/agent";
628
+ function readContractOptions(args) {
629
+ const options = {
630
+ root: typeof args.root === "string" ? args.root : process.cwd()
631
+ };
632
+ if (typeof args.manifestPath === "string") {
633
+ options.manifestPath = args.manifestPath;
634
+ }
635
+ return options;
636
+ }
637
+ function readName(args) {
638
+ const name = args.name;
639
+ if (typeof name !== "string" || name.trim().length === 0) {
640
+ throw new Error('Missing required component name. Pass a registry id such as "button".');
641
+ }
642
+ return name;
643
+ }
644
+ function validateAll(args) {
645
+ const options = readContractOptions(args);
646
+ const manifest = loadAgentManifest(options);
647
+ const results = manifest.components.map(
648
+ (component) => validateAgentComponent(loadAgentComponentContract(component.name, options))
649
+ );
650
+ const invalid = results.filter((result) => !result.valid).length;
651
+ return {
652
+ valid: invalid === 0,
653
+ total: results.length,
654
+ invalid,
655
+ results
656
+ };
657
+ }
658
+ function createUiAgentMcpServer() {
659
+ return {
660
+ id: "nebutra-ui-agent",
661
+ name: "Nebutra UI Agent Contract",
662
+ description: "Read-only tools for @nebutra/ui agent contracts, production evidence, and migration hints.",
663
+ endpoint: "local://nebutra-ui-agent",
664
+ transport: "local",
665
+ manifest: {
666
+ name: "nebutra-ui-agent",
667
+ version: "1.0.0",
668
+ scopes: ["ui:read"],
669
+ homepage: "https://ui.nebutra.com"
670
+ },
671
+ tools: [
672
+ {
673
+ name: "nebutra_ui_search_components",
674
+ description: "Search the Nebutra UI agent manifest by query, tag, status, or maturity.",
675
+ parameters: {
676
+ query: { type: "string", description: "Search text such as button, table, or chat" },
677
+ limit: { type: "number", description: "Maximum results to return", default: 20 },
678
+ offset: { type: "number", description: "Pagination offset", default: 0 },
679
+ tag: { type: "string", description: "Optional generated tag filter" },
680
+ status: { type: "string", description: "Optional status filter" },
681
+ maturity: { type: "string", description: "Optional maturity filter" },
682
+ root: { type: "string", description: "Optional Nebutra monorepo root override" },
683
+ manifestPath: {
684
+ type: "string",
685
+ description: "Optional agent-manifest.json path override"
686
+ }
687
+ },
688
+ returns: "Paginated component search results with stable component ids"
689
+ },
690
+ {
691
+ name: "nebutra_ui_get_component",
692
+ description: "Read one Nebutra UI component contract by registry id.",
693
+ parameters: {
694
+ name: { type: "string", description: "Component id, for example button", required: true },
695
+ root: { type: "string", description: "Optional Nebutra monorepo root override" },
696
+ manifestPath: {
697
+ type: "string",
698
+ description: "Optional agent-manifest.json path override"
699
+ }
700
+ },
701
+ returns: "Full component contract with imports, evidence, tokens, docs, and migration policy"
702
+ },
703
+ {
704
+ name: "nebutra_ui_validate_component",
705
+ description: "Validate production evidence for one component or the full UI contract.",
706
+ parameters: {
707
+ name: { type: "string", description: "Optional component id; omit to validate all" },
708
+ root: { type: "string", description: "Optional Nebutra monorepo root override" },
709
+ manifestPath: {
710
+ type: "string",
711
+ description: "Optional agent-manifest.json path override"
712
+ }
713
+ },
714
+ returns: "Validation result with errors and warnings"
715
+ },
716
+ {
717
+ name: "nebutra_ui_get_migration_hints",
718
+ description: "Read dry-run codemod and migration hints for one component.",
719
+ parameters: {
720
+ name: { type: "string", description: "Component id, for example button", required: true },
721
+ root: { type: "string", description: "Optional Nebutra monorepo root override" },
722
+ manifestPath: {
723
+ type: "string",
724
+ description: "Optional agent-manifest.json path override"
725
+ }
726
+ },
727
+ returns: "Migration hints and registered codemod ids"
728
+ }
729
+ ],
730
+ handlers: {
731
+ nebutra_ui_search_components: async (args) => {
732
+ const manifest = loadAgentManifest(readContractOptions(args));
733
+ const searchOptions = {
734
+ limit: typeof args.limit === "number" ? args.limit : 20,
735
+ offset: typeof args.offset === "number" ? args.offset : 0
736
+ };
737
+ if (typeof args.tag === "string") searchOptions.tag = args.tag;
738
+ if (typeof args.status === "string") searchOptions.status = args.status;
739
+ if (typeof args.maturity === "string") {
740
+ searchOptions.maturity = args.maturity;
741
+ }
742
+ return searchAgentComponents(
743
+ manifest,
744
+ typeof args.query === "string" ? args.query : "",
745
+ searchOptions
746
+ );
747
+ },
748
+ nebutra_ui_get_component: async (args) => loadAgentComponentContract(readName(args), readContractOptions(args)),
749
+ nebutra_ui_validate_component: async (args) => {
750
+ if (typeof args.name === "string" && args.name.trim().length > 0) {
751
+ return validateAgentComponent(
752
+ loadAgentComponentContract(args.name, readContractOptions(args))
753
+ );
754
+ }
755
+ return validateAll(args);
756
+ },
757
+ nebutra_ui_get_migration_hints: async (args) => {
758
+ const contract = loadAgentComponentContract(readName(args), readContractOptions(args));
759
+ return {
760
+ component: contract.name,
761
+ dryRun: true,
762
+ codemods: contract.migration.codemods,
763
+ hints: contract.migration.hints,
764
+ requiredForBreakingChanges: contract.migration.requiredForBreakingChanges
765
+ };
766
+ }
767
+ },
768
+ rateLimit: { maxRequests: 300, windowMs: 6e4 }
769
+ };
770
+ }
771
+
609
772
  // src/server/internalServers.ts
610
773
  var INTERNAL_SERVERS = [
774
+ createUiAgentMcpServer(),
611
775
  // AI Service
612
776
  {
613
777
  id: "nebutra-ai",
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  McpHost,
3
3
  readToolDebug
4
- } from "./chunk-73DKWF2B.js";
4
+ } from "./chunk-25TNKUCR.js";
5
5
  import {
6
6
  createContextServerHandlers
7
7
  } from "./chunk-YMTQFRV2.js";
package/dist/index.d.ts CHANGED
@@ -170,6 +170,8 @@ declare const serverRegistry: MCPServerRegistry;
170
170
  declare class MCPClient {
171
171
  private readonly registry;
172
172
  private requestCounter;
173
+ /** One SDK client per HTTP server id, reused across calls. */
174
+ private readonly httpClients;
173
175
  constructor(registry?: MCPServerRegistry);
174
176
  /**
175
177
  * Execute a tool by name
@@ -180,9 +182,20 @@ declare class MCPClient {
180
182
  */
181
183
  private executeOnServer;
182
184
  /**
183
- * Execute via HTTP transport
185
+ * Execute via HTTP transport, using the official MCP SDK's
186
+ * StreamableHTTPClientTransport + Client to perform the `tools/call`.
184
187
  */
185
188
  private executeHttp;
189
+ /**
190
+ * Lazily create and cache an SDK Client connected over StreamableHTTP for a
191
+ * given server. Authentication headers are derived from the same env vars
192
+ * the previous hand-written transport used.
193
+ */
194
+ private getHttpClient;
195
+ /**
196
+ * Close all cached SDK HTTP clients and clear the cache.
197
+ */
198
+ close(): Promise<void>;
186
199
  /**
187
200
  * Execute via WebSocket transport
188
201
  */
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  serverRegistry,
20
20
  toolConsentStore,
21
21
  toolDebugPath
22
- } from "./chunk-73DKWF2B.js";
22
+ } from "./chunk-25TNKUCR.js";
23
23
  export {
24
24
  INTERNAL_SERVERS,
25
25
  InMemoryToolConsentStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,10 +35,11 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "1.29.0",
37
37
  "zod": "^4.3.6",
38
- "@nebutra/capability-kit": "0.2.1",
39
- "@nebutra/errors": "0.1.1",
40
- "@nebutra/event-log": "0.1.1",
41
- "@nebutra/trace-store": "0.1.1"
38
+ "@nebutra/capability-kit": "0.2.2",
39
+ "@nebutra/errors": "0.1.2",
40
+ "@nebutra/event-log": "0.1.2",
41
+ "@nebutra/ui": "0.2.2",
42
+ "@nebutra/trace-store": "0.1.2"
42
43
  },
43
44
  "devDependencies": {
44
45
  "typescript": "^5.9.3"
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { afterEach, describe, expect, it } from "vitest";
5
6
  import { MCPClient } from "../client/mcpClient";
6
7
  import { InMemoryToolConsentStore } from "../consent";
@@ -8,6 +9,9 @@ import { readToolDebug } from "../debug";
8
9
  import { McpHost } from "../host";
9
10
  import { MCPServerRegistry } from "../registry/serverRegistry";
10
11
  import { createContextServerHandlers } from "../server/contextServer";
12
+ import { createUiAgentMcpServer } from "../server/uiAgentServer";
13
+
14
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../..");
11
15
 
12
16
  describe("tool protocol host", () => {
13
17
  it("requires manifest and per-tool consent before executing a connected server", async () => {
@@ -187,6 +191,63 @@ describe("MCP tool registry access control", () => {
187
191
  });
188
192
  });
189
193
 
194
+ describe("Nebutra UI agent MCP tools", () => {
195
+ it("searches, validates, and returns migration hints from the generated UI contract", async () => {
196
+ const registry = new MCPServerRegistry();
197
+ registry.register(createUiAgentMcpServer());
198
+ const client = new MCPClient(registry);
199
+ const context = { requestId: "ui_1" };
200
+
201
+ expect(client.listTools(context).map((tool) => tool.name)).toEqual(
202
+ expect.arrayContaining([
203
+ "nebutra_ui_search_components",
204
+ "nebutra_ui_get_component",
205
+ "nebutra_ui_validate_component",
206
+ "nebutra_ui_get_migration_hints",
207
+ ]),
208
+ );
209
+
210
+ await expect(
211
+ client.executeTool(
212
+ "nebutra_ui_search_components",
213
+ { query: "button", limit: 1, root: repoRoot },
214
+ context,
215
+ ),
216
+ ).resolves.toMatchObject({
217
+ success: true,
218
+ result: {
219
+ items: [expect.objectContaining({ name: "button" })],
220
+ },
221
+ });
222
+
223
+ await expect(
224
+ client.executeTool(
225
+ "nebutra_ui_validate_component",
226
+ { name: "button", root: repoRoot },
227
+ context,
228
+ ),
229
+ ).resolves.toMatchObject({
230
+ success: true,
231
+ result: { name: "button", valid: true },
232
+ });
233
+
234
+ await expect(
235
+ client.executeTool(
236
+ "nebutra_ui_get_migration_hints",
237
+ { name: "button", root: repoRoot },
238
+ context,
239
+ ),
240
+ ).resolves.toMatchObject({
241
+ success: true,
242
+ result: {
243
+ component: "button",
244
+ dryRun: true,
245
+ hints: expect.arrayContaining([expect.stringContaining("breaking")]),
246
+ },
247
+ });
248
+ });
249
+ });
250
+
190
251
  describe("context server handlers", () => {
191
252
  const tempRoots: string[] = [];
192
253