@nebutra/mcp 0.1.1 → 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,123 +1,115 @@
1
1
  # @nebutra/mcp
2
2
 
3
- > **Status: WIP** — Not yet integrated into any production app. Do not import until this notice is removed.
3
+ Model Context Protocol primitives for Nebutra agent and tool surfaces.
4
4
 
5
- Model Context Protocol (MCP) for AI agent tool calling.
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
- ## Installation
9
+ ## Status: WIP
8
10
 
9
- ```bash
10
- pnpm add @nebutra/mcp
11
- ```
11
+ Experimental public package. The package is published for reuse and contribution
12
+ intake, but its package metadata still marks these production gaps explicitly:
12
13
 
13
- ## Features
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
14
17
 
15
- - **MCP Client** — Execute tools across registered servers
16
- - **Server Registry** — Manage internal and external MCP servers
17
- - **Middleware** — Rate limiting, audit logging, access control
18
- - **Plan-based Access** — Tool access based on subscription tier
18
+ Use it for local tools, integration prototypes, and package development. Keep
19
+ production deployments behind the application-level gateway and approval model.
19
20
 
20
- ## Architecture
21
+ ## Installation
21
22
 
22
- ```
23
- Agent → MCPClient → ServerRegistry → MCP Servers
24
- ↓
25
- Middleware (rate-limit, audit, access-control)
23
+ ```bash
24
+ pnpm add @nebutra/mcp
26
25
  ```
27
26
 
28
- ## Usage
29
-
30
- ### Initialize
31
-
32
- ```typescript
33
- import { mcpClient, registerInternalServers } from "@nebutra/mcp";
34
-
35
- // Register built-in servers
36
- registerInternalServers();
37
- ```
27
+ ## Exports
38
28
 
39
- ### Execute Tools
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 |
40
35
 
41
- ```typescript
42
- const result = await mcpClient.executeTool(
43
- "generate_text",
44
- { prompt: "Write a product description" },
45
- {
46
- requestId: "req-123",
47
- tenantId: "org_456",
48
- plan: "PRO",
49
- },
50
- );
51
- ```
52
-
53
- ### Available Tools
54
-
55
- | Tool | Plan | Description |
56
- | ----------------- | ---------- | ------------------- |
57
- | `generate_text` | FREE | AI text generation |
58
- | `translate` | FREE | AI translation |
59
- | `embed_text` | FREE | Generate embeddings |
60
- | `recommend_items` | PRO | Get recommendations |
61
- | `sync_products` | PRO | E-commerce sync |
62
- | `mint_nft` | ENTERPRISE | Web3 minting |
63
-
64
- ## Plan-based Access Control
65
-
66
- ```typescript
67
- const plans = {
68
- FREE: ["generate_text", "translate", "embed_text", "content_*"],
69
- PRO: ["*_recommend_*", "*_ecommerce_*", ...FREE],
70
- ENTERPRISE: ["*_web3_*", ...PRO],
71
- };
72
- ```
36
+ ## Usage
73
37
 
74
- ## Register Custom Server
38
+ ### Register a local server
75
39
 
76
- ```typescript
77
- import { serverRegistry } from "@nebutra/mcp";
40
+ ```ts
41
+ import { MCPClient, serverRegistry } from "@nebutra/mcp";
78
42
 
79
43
  serverRegistry.register({
80
- name: "custom-tools",
81
- url: "http://localhost:3001/mcp",
44
+ id: "workspace",
45
+ name: "Workspace tools",
46
+ description: "Local workspace utility tools",
47
+ endpoint: "local",
48
+ transport: "local",
82
49
  tools: [
83
50
  {
84
- name: "custom_tool",
85
- description: "My custom tool",
86
- 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"],
87
57
  },
88
58
  ],
59
+ handlers: {
60
+ echo: (args) => ({ message: args.message }),
61
+ },
89
62
  allowedPlans: ["PRO", "ENTERPRISE"],
90
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
+ );
91
76
  ```
92
77
 
93
- ## Middleware
78
+ ### Register built-in servers
94
79
 
95
- ### Rate Limiting
80
+ ```ts
81
+ import { getInternalServerIds, registerInternalServers } from "@nebutra/mcp";
96
82
 
97
- ```typescript
98
- // Configured per tool
99
- const limits = {
100
- generate_text: { requests: 100, window: "1m" },
101
- embed_text: { requests: 1000, window: "1h" },
102
- };
83
+ registerInternalServers();
84
+ const internalServerIds = getInternalServerIds();
103
85
  ```
104
86
 
105
- ### 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
+ ```
106
103
 
107
- All tool executions are logged:
104
+ ## Runtime Notes
108
105
 
109
- ```json
110
- {
111
- "timestamp": "2024-01-15T10:30:00Z",
112
- "tool": "generate_text",
113
- "tenantId": "org_456",
114
- "userId": "user_123",
115
- "duration": 245,
116
- "success": true
117
- }
118
- ```
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.
119
112
 
120
- ## Related
113
+ ## License
121
114
 
122
- - [AI service](../../backends/python/ai/)
123
- - [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
@@ -295,32 +307,25 @@ var InMemoryToolConsentStore = class {
295
307
  };
296
308
 
297
309
  // src/debug.ts
298
- import { mkdir, readFile, writeFile } from "fs/promises";
299
- import { dirname, join } from "path";
310
+ import {
311
+ appendCapabilityDebug,
312
+ capabilityDebugPath,
313
+ readCapabilityDebug
314
+ } from "@nebutra/capability-kit/debug";
300
315
  function toolDebugPath() {
301
- return join(process.cwd(), ".nebutra", "debug", "tool-protocol.jsonl");
316
+ return capabilityDebugPath("tool-protocol");
302
317
  }
303
318
  async function appendToolDebug(entry) {
304
- const path = toolDebugPath();
305
- await mkdir(dirname(path), { recursive: true });
306
- await writeFile(path, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
307
- `, {
308
- flag: "a"
309
- });
319
+ await appendCapabilityDebug("tool-protocol", entry);
310
320
  }
311
321
  async function readToolDebug(limit = 10) {
312
- try {
313
- const raw = await readFile(toolDebugPath(), "utf8");
314
- return raw.trim().split("\n").filter(Boolean).slice(-limit).map((line) => JSON.parse(line));
315
- } catch {
316
- return [];
317
- }
322
+ return readCapabilityDebug("tool-protocol", { limit });
318
323
  }
319
324
 
320
325
  // src/host.ts
321
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
326
+ import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
322
327
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
323
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
328
+ import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
324
329
  import { CapabilityError } from "@nebutra/errors";
325
330
  function shortToolName(serverId, toolName) {
326
331
  return toolName.startsWith(`${serverId}:`) ? toolName.slice(serverId.length + 1) : toolName;
@@ -375,7 +380,7 @@ var McpHost = class {
375
380
  await this.#connectSdkClient(options, transport, `stdio://${command}`);
376
381
  }
377
382
  async connectStreamableHttp(endpoint, options) {
378
- const transport = new StreamableHTTPClientTransport(
383
+ const transport = new StreamableHTTPClientTransport2(
379
384
  new URL(endpoint),
380
385
  options.headers !== void 0 ? { requestInit: { headers: options.headers } } : {}
381
386
  );
@@ -475,7 +480,7 @@ var McpHost = class {
475
480
  }
476
481
  async #connectSdkClient(options, transport, endpoint) {
477
482
  const manifest = requireManifest(options.id, options.manifest);
478
- 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" });
479
484
  await client.connect(transport);
480
485
  const listed = await client.listTools();
481
486
  const tools = listed.tools.map(
@@ -613,8 +618,160 @@ function composeMCPMiddleware(middlewares) {
613
618
  };
614
619
  }
615
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
+
616
772
  // src/server/internalServers.ts
617
773
  var INTERNAL_SERVERS = [
774
+ createUiAgentMcpServer(),
618
775
  // AI Service
619
776
  {
620
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-ORGYGQJV.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-ORGYGQJV.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.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,6 +17,7 @@
17
17
  ],
18
18
  "featureId": "mcp",
19
19
  "category": "ai",
20
+ "surface": "tool-protocol",
20
21
  "summary": "MCP server primitives (Model Context Protocol)"
21
22
  },
22
23
  "main": "./src/index.ts",
@@ -32,11 +33,13 @@
32
33
  "nebutra-tool-protocol": "./dist/cli.js"
33
34
  },
34
35
  "dependencies": {
35
- "@modelcontextprotocol/sdk": "1.27.1",
36
+ "@modelcontextprotocol/sdk": "1.29.0",
36
37
  "zod": "^4.3.6",
37
- "@nebutra/errors": "0.1.0",
38
- "@nebutra/event-log": "0.1.0",
39
- "@nebutra/trace-store": "0.1.0"
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"
40
43
  },
41
44
  "devDependencies": {
42
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