@nebutra/mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +26 -0
- package/README.md +2 -0
- package/dist/chunk-73DKWF2B.js +704 -0
- package/dist/chunk-YMTQFRV2.js +178 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/index.d.ts +378 -0
- package/dist/index.js +44 -0
- package/dist/server/contextServer.d.ts +50 -0
- package/dist/server/contextServer.js +8 -0
- package/examples/doctor.ts +14 -0
- package/examples/local-consent.ts +30 -0
- package/examples/read-debug.ts +5 -0
- package/package.json +14 -7
- package/src/__tests__/registry-and-context.test.ts +234 -0
- package/src/cli.ts +47 -0
- package/src/client/mcpClient.ts +26 -5
- package/src/consent.ts +46 -0
- package/src/debug.ts +32 -0
- package/src/host.ts +314 -0
- package/src/index.ts +7 -0
- package/src/registry/serverRegistry.ts +38 -1
- package/src/server/contextServer.ts +130 -57
- package/src/types.ts +18 -1
- package/tsup.config.ts +7 -1
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
// src/registry/serverRegistry.ts
|
|
2
|
+
var MCPServerRegistry = class {
|
|
3
|
+
servers = /* @__PURE__ */ new Map();
|
|
4
|
+
toolIndex = /* @__PURE__ */ new Map();
|
|
5
|
+
// tool name -> server id
|
|
6
|
+
/**
|
|
7
|
+
* Register an MCP server
|
|
8
|
+
*/
|
|
9
|
+
register(config) {
|
|
10
|
+
this.servers.set(config.id, config);
|
|
11
|
+
for (const tool of config.tools) {
|
|
12
|
+
const qualifiedName = `${config.id}:${tool.name}`;
|
|
13
|
+
this.toolIndex.set(qualifiedName, config.id);
|
|
14
|
+
if (!this.toolIndex.has(tool.name)) {
|
|
15
|
+
this.toolIndex.set(tool.name, config.id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Unregister an MCP server
|
|
21
|
+
*/
|
|
22
|
+
unregister(serverId) {
|
|
23
|
+
const server = this.servers.get(serverId);
|
|
24
|
+
if (!server) return false;
|
|
25
|
+
for (const tool of server.tools) {
|
|
26
|
+
const qualifiedName = `${serverId}:${tool.name}`;
|
|
27
|
+
this.toolIndex.delete(qualifiedName);
|
|
28
|
+
if (this.toolIndex.get(tool.name) === serverId) {
|
|
29
|
+
this.toolIndex.delete(tool.name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.servers.delete(serverId);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get server by ID
|
|
37
|
+
*/
|
|
38
|
+
getServer(serverId) {
|
|
39
|
+
return this.servers.get(serverId);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find server by tool name
|
|
43
|
+
*/
|
|
44
|
+
findServerByTool(toolName) {
|
|
45
|
+
const serverId = this.toolIndex.get(toolName);
|
|
46
|
+
if (!serverId) return void 0;
|
|
47
|
+
return this.servers.get(serverId);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get all registered servers
|
|
51
|
+
*/
|
|
52
|
+
getAllServers() {
|
|
53
|
+
return Array.from(this.servers.values());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get all available tools
|
|
57
|
+
*/
|
|
58
|
+
getAllTools() {
|
|
59
|
+
const tools = [];
|
|
60
|
+
for (const server of this.servers.values()) {
|
|
61
|
+
for (const tool of server.tools) {
|
|
62
|
+
tools.push({ ...tool, serverId: server.id });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return tools;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if a context has access to a server
|
|
69
|
+
*/
|
|
70
|
+
canAccess(serverId, context) {
|
|
71
|
+
const server = this.servers.get(serverId);
|
|
72
|
+
if (!server) return false;
|
|
73
|
+
if (server.allowedPlans && server.allowedPlans.length > 0) {
|
|
74
|
+
if (!context.plan || !server.allowedPlans.includes(context.plan)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (server.allowedTenants && server.allowedTenants.length > 0) {
|
|
79
|
+
if (!context.tenantId || !server.allowedTenants.includes(context.tenantId)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check whether a context can execute a specific tool. Tool-level policy
|
|
87
|
+
* narrows the server policy and supports tenant, plan, and permission gates.
|
|
88
|
+
*/
|
|
89
|
+
canAccessTool(toolName, context) {
|
|
90
|
+
const server = this.findServerByTool(toolName);
|
|
91
|
+
if (!server || !this.canAccess(server.id, context)) return false;
|
|
92
|
+
const tool = this.findTool(server, toolName);
|
|
93
|
+
if (!tool) return false;
|
|
94
|
+
if (tool.allowedPlans && tool.allowedPlans.length > 0) {
|
|
95
|
+
if (!context.plan || !tool.allowedPlans.includes(context.plan)) return false;
|
|
96
|
+
}
|
|
97
|
+
if (tool.allowedTenants && tool.allowedTenants.length > 0) {
|
|
98
|
+
if (!context.tenantId || !tool.allowedTenants.includes(context.tenantId)) return false;
|
|
99
|
+
}
|
|
100
|
+
if (tool.requiredPermissions && tool.requiredPermissions.length > 0) {
|
|
101
|
+
const permissions = new Set(context.permissions ?? []);
|
|
102
|
+
if (!tool.requiredPermissions.every((permission) => permissions.has(permission))) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get tools accessible by a context
|
|
110
|
+
*/
|
|
111
|
+
getAccessibleTools(context) {
|
|
112
|
+
const tools = [];
|
|
113
|
+
for (const server of this.servers.values()) {
|
|
114
|
+
if (this.canAccess(server.id, context)) {
|
|
115
|
+
for (const tool of server.tools) {
|
|
116
|
+
if (this.canAccessTool(`${server.id}:${tool.name}`, context)) {
|
|
117
|
+
tools.push({ ...tool, serverId: server.id });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return tools;
|
|
123
|
+
}
|
|
124
|
+
findTool(server, toolName) {
|
|
125
|
+
return server.tools.find(
|
|
126
|
+
(tool) => tool.name === toolName || `${server.id}:${tool.name}` === toolName
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var serverRegistry = new MCPServerRegistry();
|
|
131
|
+
|
|
132
|
+
// src/client/mcpClient.ts
|
|
133
|
+
var MCPClient = class {
|
|
134
|
+
constructor(registry = serverRegistry) {
|
|
135
|
+
this.registry = registry;
|
|
136
|
+
}
|
|
137
|
+
requestCounter = 0;
|
|
138
|
+
/**
|
|
139
|
+
* Execute a tool by name
|
|
140
|
+
*/
|
|
141
|
+
async executeTool(toolName, args, context) {
|
|
142
|
+
const startTime = Date.now();
|
|
143
|
+
const server = this.registry.findServerByTool(toolName);
|
|
144
|
+
if (!server) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
error: `Tool not found: ${toolName}`,
|
|
148
|
+
duration: Date.now() - startTime
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!this.registry.canAccessTool(toolName, context)) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: `Access denied to tool: ${toolName}`,
|
|
155
|
+
duration: Date.now() - startTime
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const result = await this.executeOnServer(server, toolName, args, context);
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
result,
|
|
163
|
+
duration: Date.now() - startTime
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
duration: Date.now() - startTime
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Execute tool on a specific server
|
|
175
|
+
*/
|
|
176
|
+
async executeOnServer(server, toolName, args, context) {
|
|
177
|
+
const requestId = `${context.requestId}-${++this.requestCounter}`;
|
|
178
|
+
switch (server.transport) {
|
|
179
|
+
case "http":
|
|
180
|
+
return this.executeHttp(server, toolName, args, requestId);
|
|
181
|
+
case "websocket":
|
|
182
|
+
return this.executeWebSocket(server, toolName, args, requestId);
|
|
183
|
+
case "local":
|
|
184
|
+
return this.executeLocal(server, toolName, args, context);
|
|
185
|
+
case "stdio":
|
|
186
|
+
throw new Error("stdio transport not supported in browser/serverless");
|
|
187
|
+
default:
|
|
188
|
+
throw new Error(`Unknown transport: ${server.transport}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Execute via HTTP transport
|
|
193
|
+
*/
|
|
194
|
+
async executeHttp(server, toolName, args, requestId) {
|
|
195
|
+
const headers = {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
"X-Request-ID": requestId
|
|
198
|
+
};
|
|
199
|
+
if (server.authentication?.type === "bearer") {
|
|
200
|
+
const token = process.env[`MCP_${server.id.toUpperCase()}_TOKEN`];
|
|
201
|
+
if (token) {
|
|
202
|
+
headers.Authorization = `Bearer ${token}`;
|
|
203
|
+
}
|
|
204
|
+
} else if (server.authentication?.type === "api-key") {
|
|
205
|
+
const apiKey = process.env[`MCP_${server.id.toUpperCase()}_KEY`];
|
|
206
|
+
const headerName = server.authentication.headerName || "X-API-Key";
|
|
207
|
+
if (apiKey) {
|
|
208
|
+
headers[headerName] = apiKey;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
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
|
+
})
|
|
223
|
+
});
|
|
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;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Execute via WebSocket transport
|
|
235
|
+
*/
|
|
236
|
+
async executeWebSocket(_server, _toolName, _args, _requestId) {
|
|
237
|
+
throw new Error("WebSocket transport not yet implemented");
|
|
238
|
+
}
|
|
239
|
+
async executeLocal(server, toolName, args, context) {
|
|
240
|
+
const shortName = toolName.startsWith(`${server.id}:`) ? toolName.slice(server.id.length + 1) : toolName;
|
|
241
|
+
const handler = server.handlers?.[shortName] ?? server.handlers?.[toolName];
|
|
242
|
+
if (!handler) {
|
|
243
|
+
throw new Error(`No local handler registered for tool: ${toolName}`);
|
|
244
|
+
}
|
|
245
|
+
return handler(args, context);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* List available tools for a context
|
|
249
|
+
*/
|
|
250
|
+
listTools(context) {
|
|
251
|
+
return this.registry.getAccessibleTools(context);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get tool definition
|
|
255
|
+
*/
|
|
256
|
+
getTool(toolName, context) {
|
|
257
|
+
const server = this.registry.findServerByTool(toolName);
|
|
258
|
+
if (!server) return void 0;
|
|
259
|
+
if (!this.registry.canAccessTool(toolName, context)) {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
return server.tools.find((t) => t.name === toolName || `${server.id}:${t.name}` === toolName);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
var mcpClient = new MCPClient();
|
|
266
|
+
|
|
267
|
+
// src/consent.ts
|
|
268
|
+
function consentKey(grant) {
|
|
269
|
+
return [grant.tenantId, grant.userId ?? "*", grant.serverId, grant.toolName].join(":");
|
|
270
|
+
}
|
|
271
|
+
var InMemoryToolConsentStore = class {
|
|
272
|
+
#grants = /* @__PURE__ */ new Map();
|
|
273
|
+
async grant(grant) {
|
|
274
|
+
this.#grants.set(consentKey(grant), { ...grant, scopes: [...grant.scopes] });
|
|
275
|
+
}
|
|
276
|
+
async hasConsent(grant) {
|
|
277
|
+
const exact = this.#grants.get(consentKey(grant));
|
|
278
|
+
const wildcard = this.#grants.get(
|
|
279
|
+
consentKey({
|
|
280
|
+
tenantId: grant.tenantId,
|
|
281
|
+
serverId: grant.serverId,
|
|
282
|
+
toolName: grant.toolName
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
const existing = exact ?? wildcard;
|
|
286
|
+
if (!existing) return false;
|
|
287
|
+
const scopes = new Set(existing.scopes);
|
|
288
|
+
return grant.scopes.every((scope) => scopes.has(scope));
|
|
289
|
+
}
|
|
290
|
+
async list(tenantId) {
|
|
291
|
+
return Array.from(this.#grants.values()).filter(
|
|
292
|
+
(grant) => tenantId === void 0 || grant.tenantId === tenantId
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/debug.ts
|
|
298
|
+
import {
|
|
299
|
+
appendCapabilityDebug,
|
|
300
|
+
capabilityDebugPath,
|
|
301
|
+
readCapabilityDebug
|
|
302
|
+
} from "@nebutra/capability-kit/debug";
|
|
303
|
+
function toolDebugPath() {
|
|
304
|
+
return capabilityDebugPath("tool-protocol");
|
|
305
|
+
}
|
|
306
|
+
async function appendToolDebug(entry) {
|
|
307
|
+
await appendCapabilityDebug("tool-protocol", entry);
|
|
308
|
+
}
|
|
309
|
+
async function readToolDebug(limit = 10) {
|
|
310
|
+
return readCapabilityDebug("tool-protocol", { limit });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/host.ts
|
|
314
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
315
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
316
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
317
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
318
|
+
function shortToolName(serverId, toolName) {
|
|
319
|
+
return toolName.startsWith(`${serverId}:`) ? toolName.slice(serverId.length + 1) : toolName;
|
|
320
|
+
}
|
|
321
|
+
function requireManifest(serverId, manifest) {
|
|
322
|
+
if (!manifest || manifest.scopes.length === 0 || manifest.version.length === 0) {
|
|
323
|
+
throw new CapabilityError("tool-protocol", "MCP server manifest is required", {
|
|
324
|
+
suggestion: "Register every connected server with name, version, and declared scopes.",
|
|
325
|
+
metadata: { serverId },
|
|
326
|
+
statusCode: 400
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return manifest;
|
|
330
|
+
}
|
|
331
|
+
function toJsonObject(value) {
|
|
332
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : { value };
|
|
333
|
+
}
|
|
334
|
+
var McpHost = class {
|
|
335
|
+
#registry;
|
|
336
|
+
#client;
|
|
337
|
+
#consent;
|
|
338
|
+
#traceStore;
|
|
339
|
+
#eventLog;
|
|
340
|
+
#sdkClients = /* @__PURE__ */ new Map();
|
|
341
|
+
constructor(options = {}) {
|
|
342
|
+
this.#registry = options.registry ?? new MCPServerRegistry();
|
|
343
|
+
this.#client = new MCPClient(this.#registry);
|
|
344
|
+
this.#consent = options.consent ?? new InMemoryToolConsentStore();
|
|
345
|
+
this.#traceStore = options.traceStore;
|
|
346
|
+
this.#eventLog = options.eventLog;
|
|
347
|
+
}
|
|
348
|
+
get registry() {
|
|
349
|
+
return this.#registry;
|
|
350
|
+
}
|
|
351
|
+
connectLocal(config) {
|
|
352
|
+
const manifest = requireManifest(config.id, config.manifest);
|
|
353
|
+
this.#registry.register({
|
|
354
|
+
id: config.id,
|
|
355
|
+
name: config.name,
|
|
356
|
+
description: config.description,
|
|
357
|
+
endpoint: `local://${config.id}`,
|
|
358
|
+
transport: "local",
|
|
359
|
+
manifest,
|
|
360
|
+
tools: [...config.tools],
|
|
361
|
+
handlers: config.handlers,
|
|
362
|
+
...config.allowedPlans !== void 0 && { allowedPlans: [...config.allowedPlans] },
|
|
363
|
+
...config.allowedTenants !== void 0 && { allowedTenants: [...config.allowedTenants] }
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async connectStdio(command, args, options) {
|
|
367
|
+
const transport = new StdioClientTransport({ command, args: [...args] });
|
|
368
|
+
await this.#connectSdkClient(options, transport, `stdio://${command}`);
|
|
369
|
+
}
|
|
370
|
+
async connectStreamableHttp(endpoint, options) {
|
|
371
|
+
const transport = new StreamableHTTPClientTransport(
|
|
372
|
+
new URL(endpoint),
|
|
373
|
+
options.headers !== void 0 ? { requestInit: { headers: options.headers } } : {}
|
|
374
|
+
);
|
|
375
|
+
await this.#connectSdkClient(options, transport, endpoint);
|
|
376
|
+
}
|
|
377
|
+
async callTool(toolName, args, context) {
|
|
378
|
+
const start = Date.now();
|
|
379
|
+
const server = this.#registry.findServerByTool(toolName);
|
|
380
|
+
const serverId = server?.id;
|
|
381
|
+
const shortName = serverId ? shortToolName(serverId, toolName) : toolName;
|
|
382
|
+
const span = this.#traceStore?.start("tool", toolName, {
|
|
383
|
+
traceId: context.requestId,
|
|
384
|
+
tenantId: context.tenantId,
|
|
385
|
+
serverId
|
|
386
|
+
});
|
|
387
|
+
if (!context.tenantId) {
|
|
388
|
+
const result2 = {
|
|
389
|
+
success: false,
|
|
390
|
+
error: "Missing tenant context; suggestion: call tools with a tenantId.",
|
|
391
|
+
duration: Date.now() - start
|
|
392
|
+
};
|
|
393
|
+
await this.#recordCall(toolName, shortName, serverId, context, result2);
|
|
394
|
+
span?.fail(result2.error);
|
|
395
|
+
return result2;
|
|
396
|
+
}
|
|
397
|
+
if (!server || !server.manifest) {
|
|
398
|
+
const result2 = {
|
|
399
|
+
success: false,
|
|
400
|
+
error: "Tool server is not connected with a manifest; suggestion: run `pnpm tool:doctor`.",
|
|
401
|
+
duration: Date.now() - start
|
|
402
|
+
};
|
|
403
|
+
await this.#recordCall(toolName, shortName, serverId, context, result2);
|
|
404
|
+
span?.fail(result2.error);
|
|
405
|
+
return result2;
|
|
406
|
+
}
|
|
407
|
+
const consentRequest = {
|
|
408
|
+
tenantId: context.tenantId,
|
|
409
|
+
serverId: server.id,
|
|
410
|
+
toolName: shortName,
|
|
411
|
+
scopes: server.manifest.scopes,
|
|
412
|
+
...context.userId !== void 0 && { userId: context.userId }
|
|
413
|
+
};
|
|
414
|
+
const hasConsent = await this.#consent.hasConsent(consentRequest);
|
|
415
|
+
if (!hasConsent) {
|
|
416
|
+
const result2 = {
|
|
417
|
+
success: false,
|
|
418
|
+
error: `Missing per-tool consent for ${server.id}:${shortName}; suggestion: grant scopes ${server.manifest.scopes.join(", ")}.`,
|
|
419
|
+
duration: Date.now() - start
|
|
420
|
+
};
|
|
421
|
+
await this.#recordCall(toolName, shortName, server.id, context, result2);
|
|
422
|
+
span?.fail(result2.error);
|
|
423
|
+
return result2;
|
|
424
|
+
}
|
|
425
|
+
const result = await this.#client.executeTool(toolName, args, context);
|
|
426
|
+
await this.#recordCall(toolName, shortName, server.id, context, result);
|
|
427
|
+
if (result.success) span?.end({ toolName });
|
|
428
|
+
else span?.fail(result.error);
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
listTools(context) {
|
|
432
|
+
return this.#client.listTools(context);
|
|
433
|
+
}
|
|
434
|
+
inspect(serverId) {
|
|
435
|
+
const server = this.#registry.getServer(serverId);
|
|
436
|
+
if (!server) {
|
|
437
|
+
throw new CapabilityError("tool-protocol", "MCP server not found", {
|
|
438
|
+
suggestion: "Run `pnpm tool:doctor` to list connected servers.",
|
|
439
|
+
metadata: { serverId },
|
|
440
|
+
statusCode: 404
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
void appendToolDebug({ type: "inspect", serverId, ok: true });
|
|
444
|
+
return server;
|
|
445
|
+
}
|
|
446
|
+
async doctor() {
|
|
447
|
+
const servers = this.#registry.getAllServers().map((server) => ({
|
|
448
|
+
id: server.id,
|
|
449
|
+
name: server.name,
|
|
450
|
+
transport: server.transport,
|
|
451
|
+
tools: server.tools.length,
|
|
452
|
+
...server.manifest !== void 0 && { manifest: server.manifest }
|
|
453
|
+
}));
|
|
454
|
+
return {
|
|
455
|
+
ok: servers.length > 0,
|
|
456
|
+
servers,
|
|
457
|
+
...servers.length === 0 && {
|
|
458
|
+
suggestion: "Connect an MCP server or register a local tool surface before running agents."
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
async debug(limit = 10) {
|
|
463
|
+
return readToolDebug(limit);
|
|
464
|
+
}
|
|
465
|
+
async close() {
|
|
466
|
+
await Promise.all(Array.from(this.#sdkClients.values()).map((client) => client.close()));
|
|
467
|
+
this.#sdkClients.clear();
|
|
468
|
+
}
|
|
469
|
+
async #connectSdkClient(options, transport, endpoint) {
|
|
470
|
+
const manifest = requireManifest(options.id, options.manifest);
|
|
471
|
+
const client = new Client({ name: "nebutra-tool-protocol", version: "0.1.0" });
|
|
472
|
+
await client.connect(transport);
|
|
473
|
+
const listed = await client.listTools();
|
|
474
|
+
const tools = listed.tools.map(
|
|
475
|
+
(tool) => ({
|
|
476
|
+
name: tool.name,
|
|
477
|
+
description: tool.description ?? tool.name,
|
|
478
|
+
parameters: {}
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
const handlers = Object.fromEntries(
|
|
482
|
+
tools.map((tool) => [
|
|
483
|
+
tool.name,
|
|
484
|
+
async (args) => {
|
|
485
|
+
return client.callTool({ name: tool.name, arguments: toJsonObject(args) });
|
|
486
|
+
}
|
|
487
|
+
])
|
|
488
|
+
);
|
|
489
|
+
this.#sdkClients.set(options.id, client);
|
|
490
|
+
this.#registry.register({
|
|
491
|
+
id: options.id,
|
|
492
|
+
name: options.name ?? manifest.name,
|
|
493
|
+
description: options.description ?? `${manifest.name} MCP server`,
|
|
494
|
+
endpoint,
|
|
495
|
+
transport: endpoint.startsWith("stdio://") ? "stdio" : "http",
|
|
496
|
+
manifest,
|
|
497
|
+
tools,
|
|
498
|
+
handlers
|
|
499
|
+
});
|
|
500
|
+
await appendToolDebug({ type: "connect", serverId: options.id, ok: true });
|
|
501
|
+
}
|
|
502
|
+
async #recordCall(originalToolName, toolName, serverId, context, result) {
|
|
503
|
+
await appendToolDebug({
|
|
504
|
+
type: "tool_call",
|
|
505
|
+
requestId: context.requestId,
|
|
506
|
+
toolName,
|
|
507
|
+
ok: result.success,
|
|
508
|
+
durationMs: result.duration,
|
|
509
|
+
...context.tenantId !== void 0 && { tenantId: context.tenantId },
|
|
510
|
+
...serverId !== void 0 && { serverId },
|
|
511
|
+
...result.error !== void 0 && { error: result.error }
|
|
512
|
+
});
|
|
513
|
+
if (result.success && context.tenantId && this.#eventLog) {
|
|
514
|
+
await this.#eventLog.commit({
|
|
515
|
+
traceId: context.requestId,
|
|
516
|
+
kind: "tool_call",
|
|
517
|
+
affected: [],
|
|
518
|
+
parent: null,
|
|
519
|
+
snapshot: {
|
|
520
|
+
[`tool/${serverId ?? "unknown"}/${toolName}.json`]: JSON.stringify({
|
|
521
|
+
toolName: originalToolName,
|
|
522
|
+
result: result.result
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
var toolConsentStore = new InMemoryToolConsentStore();
|
|
530
|
+
var mcpHost = new McpHost({ consent: toolConsentStore });
|
|
531
|
+
|
|
532
|
+
// src/middleware/mcpMiddleware.ts
|
|
533
|
+
function createRateLimitMiddleware(options) {
|
|
534
|
+
const requests = /* @__PURE__ */ new Map();
|
|
535
|
+
return async (ctx, next) => {
|
|
536
|
+
const key = `${ctx.tenantId || "anon"}:${ctx.userId || "anon"}`;
|
|
537
|
+
const now = Date.now();
|
|
538
|
+
let record = requests.get(key);
|
|
539
|
+
if (!record || now > record.resetAt) {
|
|
540
|
+
record = { count: 0, resetAt: now + options.windowMs };
|
|
541
|
+
requests.set(key, record);
|
|
542
|
+
}
|
|
543
|
+
record.count++;
|
|
544
|
+
if (record.count > options.maxRequests) {
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: `MCP rate limit exceeded. Max ${options.maxRequests} requests per ${options.windowMs / 1e3}s`,
|
|
548
|
+
duration: 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return next();
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function createAuditMiddleware(options) {
|
|
555
|
+
return async (ctx, next) => {
|
|
556
|
+
const result = await next();
|
|
557
|
+
const entry = {
|
|
558
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
559
|
+
requestId: ctx.requestId,
|
|
560
|
+
toolName: ctx.toolName,
|
|
561
|
+
arguments: ctx.arguments,
|
|
562
|
+
success: result.success,
|
|
563
|
+
duration: result.duration
|
|
564
|
+
};
|
|
565
|
+
if (ctx.tenantId) entry.tenantId = ctx.tenantId;
|
|
566
|
+
if (ctx.userId) entry.userId = ctx.userId;
|
|
567
|
+
if (result.error) entry.error = result.error;
|
|
568
|
+
Promise.resolve(options.onLog(entry)).catch(console.error);
|
|
569
|
+
return result;
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function createAccessControlMiddleware(options) {
|
|
573
|
+
return async (ctx, next) => {
|
|
574
|
+
if (options.blockedTools?.includes(ctx.toolName)) {
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
error: `Tool ${ctx.toolName} is blocked`,
|
|
578
|
+
duration: 0
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (options.allowedTools && options.allowedTools.length > 0 && !options.allowedTools.includes(ctx.toolName)) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: `Tool ${ctx.toolName} is not in allowed list`,
|
|
585
|
+
duration: 0
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return next();
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function composeMCPMiddleware(middlewares) {
|
|
592
|
+
return async (ctx, finalNext) => {
|
|
593
|
+
let index = -1;
|
|
594
|
+
const dispatch = async (i) => {
|
|
595
|
+
if (i <= index) {
|
|
596
|
+
throw new Error("next() called multiple times");
|
|
597
|
+
}
|
|
598
|
+
index = i;
|
|
599
|
+
const middleware = middlewares[i];
|
|
600
|
+
if (!middleware) {
|
|
601
|
+
return finalNext();
|
|
602
|
+
}
|
|
603
|
+
return middleware(ctx, () => dispatch(i + 1));
|
|
604
|
+
};
|
|
605
|
+
return dispatch(0);
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/server/internalServers.ts
|
|
610
|
+
var INTERNAL_SERVERS = [
|
|
611
|
+
// AI Service
|
|
612
|
+
{
|
|
613
|
+
id: "nebutra-ai",
|
|
614
|
+
name: "Nebutra AI Service",
|
|
615
|
+
description: "AI-powered text generation, embedding, and translation",
|
|
616
|
+
endpoint: process.env.AI_SERVICE_URL || "http://localhost:8001",
|
|
617
|
+
transport: "http",
|
|
618
|
+
tools: [
|
|
619
|
+
{
|
|
620
|
+
name: "generate_text",
|
|
621
|
+
description: "Generate text using LLM",
|
|
622
|
+
parameters: {
|
|
623
|
+
prompt: { type: "string", description: "Input prompt", required: true },
|
|
624
|
+
max_tokens: { type: "number", description: "Max tokens to generate", default: 1e3 },
|
|
625
|
+
temperature: { type: "number", description: "Sampling temperature", default: 0.7 }
|
|
626
|
+
},
|
|
627
|
+
returns: "Generated text"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "create_embedding",
|
|
631
|
+
description: "Create vector embedding for text",
|
|
632
|
+
parameters: {
|
|
633
|
+
text: { type: "string", description: "Text to embed", required: true },
|
|
634
|
+
model: {
|
|
635
|
+
type: "string",
|
|
636
|
+
description: "Embedding model",
|
|
637
|
+
default: "text-embedding-3-small"
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
returns: "Vector embedding array"
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "translate",
|
|
644
|
+
description: "Translate text between languages",
|
|
645
|
+
parameters: {
|
|
646
|
+
text: { type: "string", description: "Text to translate", required: true },
|
|
647
|
+
source: { type: "string", description: "Source language code", default: "en" },
|
|
648
|
+
target: { type: "string", description: "Target language code", required: true }
|
|
649
|
+
},
|
|
650
|
+
returns: "Translated text"
|
|
651
|
+
}
|
|
652
|
+
],
|
|
653
|
+
rateLimit: { maxRequests: 100, windowMs: 6e4 }
|
|
654
|
+
}
|
|
655
|
+
];
|
|
656
|
+
function registerInternalServers() {
|
|
657
|
+
for (const server of INTERNAL_SERVERS) {
|
|
658
|
+
serverRegistry.register(server);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function getInternalServerIds() {
|
|
662
|
+
return INTERNAL_SERVERS.map((s) => s.id);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/types.ts
|
|
666
|
+
import { z } from "zod";
|
|
667
|
+
var ToolParameterSchema = z.object({
|
|
668
|
+
type: z.enum(["string", "number", "boolean", "object", "array"]),
|
|
669
|
+
description: z.string().optional(),
|
|
670
|
+
required: z.boolean().optional(),
|
|
671
|
+
default: z.any().optional()
|
|
672
|
+
});
|
|
673
|
+
var ToolDefinitionSchema = z.object({
|
|
674
|
+
name: z.string(),
|
|
675
|
+
description: z.string(),
|
|
676
|
+
parameters: z.record(z.string(), ToolParameterSchema).optional(),
|
|
677
|
+
returns: z.string().optional(),
|
|
678
|
+
allowedPlans: z.array(z.string()).optional(),
|
|
679
|
+
allowedTenants: z.array(z.string()).optional(),
|
|
680
|
+
requiredPermissions: z.array(z.string()).optional()
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
export {
|
|
684
|
+
MCPServerRegistry,
|
|
685
|
+
serverRegistry,
|
|
686
|
+
MCPClient,
|
|
687
|
+
mcpClient,
|
|
688
|
+
InMemoryToolConsentStore,
|
|
689
|
+
toolDebugPath,
|
|
690
|
+
appendToolDebug,
|
|
691
|
+
readToolDebug,
|
|
692
|
+
McpHost,
|
|
693
|
+
toolConsentStore,
|
|
694
|
+
mcpHost,
|
|
695
|
+
createRateLimitMiddleware,
|
|
696
|
+
createAuditMiddleware,
|
|
697
|
+
createAccessControlMiddleware,
|
|
698
|
+
composeMCPMiddleware,
|
|
699
|
+
INTERNAL_SERVERS,
|
|
700
|
+
registerInternalServers,
|
|
701
|
+
getInternalServerIds,
|
|
702
|
+
ToolParameterSchema,
|
|
703
|
+
ToolDefinitionSchema
|
|
704
|
+
};
|