@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/.turbo/turbo-build.log +19 -19
- package/.turbo/turbo-test.log +7 -7
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +39 -1
- package/LICENSE +21 -676
- package/README.md +82 -90
- package/dist/{chunk-ORGYGQJV.js → chunk-25TNKUCR.js} +198 -41
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +1 -1
- package/package.json +8 -5
- package/src/__tests__/registry-and-context.test.ts +61 -0
- package/src/client/mcpClient.ts +40 -27
- package/src/debug.ts +8 -19
- package/src/server/index.ts +1 -0
- package/src/server/internalServers.ts +2 -0
- package/src/server/uiAgentServer.ts +167 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
CHANGED
|
@@ -1,123 +1,115 @@
|
|
|
1
1
|
# @nebutra/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Model Context Protocol primitives for Nebutra agent and tool surfaces.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
9
|
+
## Status: WIP
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
##
|
|
21
|
+
## Installation
|
|
21
22
|
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
↓
|
|
25
|
-
Middleware (rate-limit, audit, access-control)
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @nebutra/mcp
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
### Register a local server
|
|
75
39
|
|
|
76
|
-
```
|
|
77
|
-
import { serverRegistry } from "@nebutra/mcp";
|
|
40
|
+
```ts
|
|
41
|
+
import { MCPClient, serverRegistry } from "@nebutra/mcp";
|
|
78
42
|
|
|
79
43
|
serverRegistry.register({
|
|
80
|
-
|
|
81
|
-
|
|
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: "
|
|
85
|
-
description: "
|
|
86
|
-
parameters: {
|
|
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
|
-
|
|
78
|
+
### Register built-in servers
|
|
94
79
|
|
|
95
|
-
|
|
80
|
+
```ts
|
|
81
|
+
import { getInternalServerIds, registerInternalServers } from "@nebutra/mcp";
|
|
96
82
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
104
|
+
## Runtime Notes
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
##
|
|
113
|
+
## License
|
|
121
114
|
|
|
122
|
-
|
|
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
|
|
212
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 {
|
|
299
|
-
|
|
310
|
+
import {
|
|
311
|
+
appendCapabilityDebug,
|
|
312
|
+
capabilityDebugPath,
|
|
313
|
+
readCapabilityDebug
|
|
314
|
+
} from "@nebutra/capability-kit/debug";
|
|
300
315
|
function toolDebugPath() {
|
|
301
|
-
return
|
|
316
|
+
return capabilityDebugPath("tool-protocol");
|
|
302
317
|
}
|
|
303
318
|
async function appendToolDebug(entry) {
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/mcp",
|
|
3
|
-
"version": "0.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.
|
|
36
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
36
37
|
"zod": "^4.3.6",
|
|
37
|
-
"@nebutra/
|
|
38
|
-
"@nebutra/
|
|
39
|
-
"@nebutra/
|
|
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
|
|