@nebutra/mcp 0.1.2 → 0.1.4
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/CHANGELOG.md +27 -0
- package/LICENSE +21 -676
- package/README.md +81 -91
- package/dist/{chunk-73DKWF2B.js → chunk-25TNKUCR.js} +190 -26
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +1 -1
- package/package.json +22 -12
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -62
- package/bin/nebutra-mcp.js +0 -5
- package/examples/doctor.ts +0 -14
- package/examples/local-consent.ts +0 -30
- package/examples/read-debug.ts +0 -5
- package/src/__tests__/registry-and-context.test.ts +0 -234
- package/src/cli.ts +0 -47
- package/src/client/index.ts +0 -1
- package/src/client/mcpClient.ts +0 -193
- package/src/consent.ts +0 -46
- package/src/debug.ts +0 -32
- package/src/host.ts +0 -314
- package/src/index.ts +0 -30
- package/src/middleware/index.ts +0 -10
- package/src/middleware/mcpMiddleware.ts +0 -150
- package/src/registry/index.ts +0 -1
- package/src/registry/serverRegistry.ts +0 -165
- package/src/server/contextServer.ts +0 -211
- package/src/server/index.ts +0 -5
- package/src/server/internalServers.ts +0 -70
- package/src/types.ts +0 -135
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -17
package/src/host.ts
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
-
import { CapabilityError } from "@nebutra/errors";
|
|
5
|
-
import type { EventLog } from "@nebutra/event-log";
|
|
6
|
-
import type { TraceStore } from "@nebutra/trace-store";
|
|
7
|
-
import { MCPClient } from "./client/mcpClient";
|
|
8
|
-
import { InMemoryToolConsentStore, type ToolConsentStore } from "./consent";
|
|
9
|
-
import { appendToolDebug, readToolDebug } from "./debug";
|
|
10
|
-
import { MCPServerRegistry } from "./registry/serverRegistry";
|
|
11
|
-
import type {
|
|
12
|
-
MCPContext,
|
|
13
|
-
MCPServerConfig,
|
|
14
|
-
MCPServerManifest,
|
|
15
|
-
MCPToolHandler,
|
|
16
|
-
ToolDefinition,
|
|
17
|
-
ToolExecutionResult,
|
|
18
|
-
} from "./types";
|
|
19
|
-
|
|
20
|
-
export interface McpHostOptions {
|
|
21
|
-
readonly registry?: MCPServerRegistry;
|
|
22
|
-
readonly consent?: ToolConsentStore;
|
|
23
|
-
readonly traceStore?: TraceStore;
|
|
24
|
-
readonly eventLog?: EventLog;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface LocalMcpServerConfig {
|
|
28
|
-
readonly id: string;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly description: string;
|
|
31
|
-
readonly manifest?: MCPServerManifest;
|
|
32
|
-
readonly tools: readonly ToolDefinition[];
|
|
33
|
-
readonly handlers: Record<string, MCPToolHandler>;
|
|
34
|
-
readonly allowedPlans?: readonly string[];
|
|
35
|
-
readonly allowedTenants?: readonly string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface RemoteMcpConnectOptions {
|
|
39
|
-
readonly id: string;
|
|
40
|
-
readonly name?: string;
|
|
41
|
-
readonly description?: string;
|
|
42
|
-
readonly manifest: MCPServerManifest;
|
|
43
|
-
readonly headers?: Record<string, string>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function shortToolName(serverId: string, toolName: string): string {
|
|
47
|
-
return toolName.startsWith(`${serverId}:`) ? toolName.slice(serverId.length + 1) : toolName;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function requireManifest(
|
|
51
|
-
serverId: string,
|
|
52
|
-
manifest: MCPServerManifest | undefined,
|
|
53
|
-
): MCPServerManifest {
|
|
54
|
-
if (!manifest || manifest.scopes.length === 0 || manifest.version.length === 0) {
|
|
55
|
-
throw new CapabilityError("tool-protocol", "MCP server manifest is required", {
|
|
56
|
-
suggestion: "Register every connected server with name, version, and declared scopes.",
|
|
57
|
-
metadata: { serverId },
|
|
58
|
-
statusCode: 400,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return manifest;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function toJsonObject(value: unknown): Record<string, unknown> {
|
|
65
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
66
|
-
? (value as Record<string, unknown>)
|
|
67
|
-
: { value };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export class McpHost {
|
|
71
|
-
readonly #registry: MCPServerRegistry;
|
|
72
|
-
readonly #client: MCPClient;
|
|
73
|
-
readonly #consent: ToolConsentStore;
|
|
74
|
-
readonly #traceStore: TraceStore | undefined;
|
|
75
|
-
readonly #eventLog: EventLog | undefined;
|
|
76
|
-
readonly #sdkClients = new Map<string, Client>();
|
|
77
|
-
|
|
78
|
-
constructor(options: McpHostOptions = {}) {
|
|
79
|
-
this.#registry = options.registry ?? new MCPServerRegistry();
|
|
80
|
-
this.#client = new MCPClient(this.#registry);
|
|
81
|
-
this.#consent = options.consent ?? new InMemoryToolConsentStore();
|
|
82
|
-
this.#traceStore = options.traceStore;
|
|
83
|
-
this.#eventLog = options.eventLog;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get registry(): MCPServerRegistry {
|
|
87
|
-
return this.#registry;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
connectLocal(config: LocalMcpServerConfig): void {
|
|
91
|
-
const manifest = requireManifest(config.id, config.manifest);
|
|
92
|
-
this.#registry.register({
|
|
93
|
-
id: config.id,
|
|
94
|
-
name: config.name,
|
|
95
|
-
description: config.description,
|
|
96
|
-
endpoint: `local://${config.id}`,
|
|
97
|
-
transport: "local",
|
|
98
|
-
manifest,
|
|
99
|
-
tools: [...config.tools],
|
|
100
|
-
handlers: config.handlers,
|
|
101
|
-
...(config.allowedPlans !== undefined && { allowedPlans: [...config.allowedPlans] }),
|
|
102
|
-
...(config.allowedTenants !== undefined && { allowedTenants: [...config.allowedTenants] }),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async connectStdio(
|
|
107
|
-
command: string,
|
|
108
|
-
args: readonly string[],
|
|
109
|
-
options: RemoteMcpConnectOptions,
|
|
110
|
-
): Promise<void> {
|
|
111
|
-
const transport = new StdioClientTransport({ command, args: [...args] });
|
|
112
|
-
await this.#connectSdkClient(options, transport, `stdio://${command}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async connectStreamableHttp(endpoint: string, options: RemoteMcpConnectOptions): Promise<void> {
|
|
116
|
-
const transport = new StreamableHTTPClientTransport(
|
|
117
|
-
new URL(endpoint),
|
|
118
|
-
options.headers !== undefined ? { requestInit: { headers: options.headers } } : {},
|
|
119
|
-
);
|
|
120
|
-
await this.#connectSdkClient(options, transport, endpoint);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async callTool(
|
|
124
|
-
toolName: string,
|
|
125
|
-
args: Record<string, unknown>,
|
|
126
|
-
context: MCPContext,
|
|
127
|
-
): Promise<ToolExecutionResult> {
|
|
128
|
-
const start = Date.now();
|
|
129
|
-
const server = this.#registry.findServerByTool(toolName);
|
|
130
|
-
const serverId = server?.id;
|
|
131
|
-
const shortName = serverId ? shortToolName(serverId, toolName) : toolName;
|
|
132
|
-
const span = this.#traceStore?.start("tool", toolName, {
|
|
133
|
-
traceId: context.requestId,
|
|
134
|
-
tenantId: context.tenantId,
|
|
135
|
-
serverId,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!context.tenantId) {
|
|
139
|
-
const result = {
|
|
140
|
-
success: false,
|
|
141
|
-
error: "Missing tenant context; suggestion: call tools with a tenantId.",
|
|
142
|
-
duration: Date.now() - start,
|
|
143
|
-
};
|
|
144
|
-
await this.#recordCall(toolName, shortName, serverId, context, result);
|
|
145
|
-
span?.fail(result.error);
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!server || !server.manifest) {
|
|
150
|
-
const result = {
|
|
151
|
-
success: false,
|
|
152
|
-
error: "Tool server is not connected with a manifest; suggestion: run `pnpm tool:doctor`.",
|
|
153
|
-
duration: Date.now() - start,
|
|
154
|
-
};
|
|
155
|
-
await this.#recordCall(toolName, shortName, serverId, context, result);
|
|
156
|
-
span?.fail(result.error);
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const consentRequest = {
|
|
161
|
-
tenantId: context.tenantId,
|
|
162
|
-
serverId: server.id,
|
|
163
|
-
toolName: shortName,
|
|
164
|
-
scopes: server.manifest.scopes,
|
|
165
|
-
...(context.userId !== undefined && { userId: context.userId }),
|
|
166
|
-
};
|
|
167
|
-
const hasConsent = await this.#consent.hasConsent(consentRequest);
|
|
168
|
-
|
|
169
|
-
if (!hasConsent) {
|
|
170
|
-
const result = {
|
|
171
|
-
success: false,
|
|
172
|
-
error: `Missing per-tool consent for ${server.id}:${shortName}; suggestion: grant scopes ${server.manifest.scopes.join(", ")}.`,
|
|
173
|
-
duration: Date.now() - start,
|
|
174
|
-
};
|
|
175
|
-
await this.#recordCall(toolName, shortName, server.id, context, result);
|
|
176
|
-
span?.fail(result.error);
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const result = await this.#client.executeTool(toolName, args, context);
|
|
181
|
-
await this.#recordCall(toolName, shortName, server.id, context, result);
|
|
182
|
-
if (result.success) span?.end({ toolName });
|
|
183
|
-
else span?.fail(result.error);
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
listTools(context: MCPContext): Array<ToolDefinition & { serverId: string }> {
|
|
188
|
-
return this.#client.listTools(context);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
inspect(serverId: string): MCPServerConfig {
|
|
192
|
-
const server = this.#registry.getServer(serverId);
|
|
193
|
-
if (!server) {
|
|
194
|
-
throw new CapabilityError("tool-protocol", "MCP server not found", {
|
|
195
|
-
suggestion: "Run `pnpm tool:doctor` to list connected servers.",
|
|
196
|
-
metadata: { serverId },
|
|
197
|
-
statusCode: 404,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
void appendToolDebug({ type: "inspect", serverId, ok: true });
|
|
201
|
-
return server;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async doctor(): Promise<{
|
|
205
|
-
ok: boolean;
|
|
206
|
-
servers: Array<{
|
|
207
|
-
id: string;
|
|
208
|
-
name: string;
|
|
209
|
-
transport: MCPServerConfig["transport"];
|
|
210
|
-
tools: number;
|
|
211
|
-
manifest?: MCPServerManifest;
|
|
212
|
-
}>;
|
|
213
|
-
suggestion?: string;
|
|
214
|
-
}> {
|
|
215
|
-
const servers = this.#registry.getAllServers().map((server) => ({
|
|
216
|
-
id: server.id,
|
|
217
|
-
name: server.name,
|
|
218
|
-
transport: server.transport,
|
|
219
|
-
tools: server.tools.length,
|
|
220
|
-
...(server.manifest !== undefined && { manifest: server.manifest }),
|
|
221
|
-
}));
|
|
222
|
-
return {
|
|
223
|
-
ok: servers.length > 0,
|
|
224
|
-
servers,
|
|
225
|
-
...(servers.length === 0 && {
|
|
226
|
-
suggestion: "Connect an MCP server or register a local tool surface before running agents.",
|
|
227
|
-
}),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async debug(limit = 10) {
|
|
232
|
-
return readToolDebug(limit);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async close(): Promise<void> {
|
|
236
|
-
await Promise.all(Array.from(this.#sdkClients.values()).map((client) => client.close()));
|
|
237
|
-
this.#sdkClients.clear();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async #connectSdkClient(
|
|
241
|
-
options: RemoteMcpConnectOptions,
|
|
242
|
-
transport: unknown,
|
|
243
|
-
endpoint: string,
|
|
244
|
-
): Promise<void> {
|
|
245
|
-
const manifest = requireManifest(options.id, options.manifest);
|
|
246
|
-
const client = new Client({ name: "nebutra-tool-protocol", version: "0.1.0" });
|
|
247
|
-
await client.connect(transport as Parameters<Client["connect"]>[0]);
|
|
248
|
-
const listed = await client.listTools();
|
|
249
|
-
const tools = listed.tools.map(
|
|
250
|
-
(tool) =>
|
|
251
|
-
({
|
|
252
|
-
name: tool.name,
|
|
253
|
-
description: tool.description ?? tool.name,
|
|
254
|
-
parameters: {},
|
|
255
|
-
}) satisfies ToolDefinition,
|
|
256
|
-
);
|
|
257
|
-
const handlers = Object.fromEntries(
|
|
258
|
-
tools.map((tool) => [
|
|
259
|
-
tool.name,
|
|
260
|
-
async (args: Record<string, unknown>) => {
|
|
261
|
-
return client.callTool({ name: tool.name, arguments: toJsonObject(args) });
|
|
262
|
-
},
|
|
263
|
-
]),
|
|
264
|
-
);
|
|
265
|
-
this.#sdkClients.set(options.id, client);
|
|
266
|
-
this.#registry.register({
|
|
267
|
-
id: options.id,
|
|
268
|
-
name: options.name ?? manifest.name,
|
|
269
|
-
description: options.description ?? `${manifest.name} MCP server`,
|
|
270
|
-
endpoint,
|
|
271
|
-
transport: endpoint.startsWith("stdio://") ? "stdio" : "http",
|
|
272
|
-
manifest,
|
|
273
|
-
tools,
|
|
274
|
-
handlers,
|
|
275
|
-
});
|
|
276
|
-
await appendToolDebug({ type: "connect", serverId: options.id, ok: true });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async #recordCall(
|
|
280
|
-
originalToolName: string,
|
|
281
|
-
toolName: string,
|
|
282
|
-
serverId: string | undefined,
|
|
283
|
-
context: MCPContext,
|
|
284
|
-
result: ToolExecutionResult,
|
|
285
|
-
): Promise<void> {
|
|
286
|
-
await appendToolDebug({
|
|
287
|
-
type: "tool_call",
|
|
288
|
-
requestId: context.requestId,
|
|
289
|
-
toolName,
|
|
290
|
-
ok: result.success,
|
|
291
|
-
durationMs: result.duration,
|
|
292
|
-
...(context.tenantId !== undefined && { tenantId: context.tenantId }),
|
|
293
|
-
...(serverId !== undefined && { serverId }),
|
|
294
|
-
...(result.error !== undefined && { error: result.error }),
|
|
295
|
-
});
|
|
296
|
-
if (result.success && context.tenantId && this.#eventLog) {
|
|
297
|
-
await this.#eventLog.commit({
|
|
298
|
-
traceId: context.requestId,
|
|
299
|
-
kind: "tool_call",
|
|
300
|
-
affected: [],
|
|
301
|
-
parent: null,
|
|
302
|
-
snapshot: {
|
|
303
|
-
[`tool/${serverId ?? "unknown"}/${toolName}.json`]: JSON.stringify({
|
|
304
|
-
toolName: originalToolName,
|
|
305
|
-
result: result.result,
|
|
306
|
-
}),
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export const toolConsentStore = new InMemoryToolConsentStore();
|
|
314
|
-
export const mcpHost = new McpHost({ consent: toolConsentStore });
|
package/src/index.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Types
|
|
2
|
-
|
|
3
|
-
// Client
|
|
4
|
-
export { MCPClient, mcpClient } from "./client/index";
|
|
5
|
-
export {
|
|
6
|
-
InMemoryToolConsentStore,
|
|
7
|
-
type ToolConsentGrant,
|
|
8
|
-
type ToolConsentStore,
|
|
9
|
-
} from "./consent";
|
|
10
|
-
export { appendToolDebug, readToolDebug, type ToolDebugEntry, toolDebugPath } from "./debug";
|
|
11
|
-
export { McpHost, mcpHost, toolConsentStore } from "./host";
|
|
12
|
-
// Middleware
|
|
13
|
-
export {
|
|
14
|
-
type AuditLogEntry,
|
|
15
|
-
composeMCPMiddleware,
|
|
16
|
-
createAccessControlMiddleware,
|
|
17
|
-
createAuditMiddleware,
|
|
18
|
-
createRateLimitMiddleware,
|
|
19
|
-
type MCPMiddleware,
|
|
20
|
-
type MCPMiddlewareContext,
|
|
21
|
-
} from "./middleware/index";
|
|
22
|
-
// Registry
|
|
23
|
-
export { MCPServerRegistry, serverRegistry } from "./registry/index";
|
|
24
|
-
// Server
|
|
25
|
-
export {
|
|
26
|
-
getInternalServerIds,
|
|
27
|
-
INTERNAL_SERVERS,
|
|
28
|
-
registerInternalServers,
|
|
29
|
-
} from "./server/index";
|
|
30
|
-
export * from "./types";
|
package/src/middleware/index.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import type { MCPContext, ToolExecutionResult } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MCP Middleware for rate limiting, audit logging, and access control
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface MCPMiddlewareContext extends MCPContext {
|
|
8
|
-
toolName: string;
|
|
9
|
-
arguments: Record<string, unknown>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type MCPMiddlewareNext = () => Promise<ToolExecutionResult>;
|
|
13
|
-
|
|
14
|
-
export type MCPMiddleware = (
|
|
15
|
-
ctx: MCPMiddlewareContext,
|
|
16
|
-
next: MCPMiddlewareNext,
|
|
17
|
-
) => Promise<ToolExecutionResult>;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Rate limiting middleware for MCP calls
|
|
21
|
-
*/
|
|
22
|
-
export function createRateLimitMiddleware(options: {
|
|
23
|
-
maxRequests: number;
|
|
24
|
-
windowMs: number;
|
|
25
|
-
}): MCPMiddleware {
|
|
26
|
-
const requests = new Map<string, { count: number; resetAt: number }>();
|
|
27
|
-
|
|
28
|
-
return async (ctx, next) => {
|
|
29
|
-
const key = `${ctx.tenantId || "anon"}:${ctx.userId || "anon"}`;
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
|
|
32
|
-
let record = requests.get(key);
|
|
33
|
-
if (!record || now > record.resetAt) {
|
|
34
|
-
record = { count: 0, resetAt: now + options.windowMs };
|
|
35
|
-
requests.set(key, record);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
record.count++;
|
|
39
|
-
|
|
40
|
-
if (record.count > options.maxRequests) {
|
|
41
|
-
return {
|
|
42
|
-
success: false,
|
|
43
|
-
error: `MCP rate limit exceeded. Max ${options.maxRequests} requests per ${options.windowMs / 1000}s`,
|
|
44
|
-
duration: 0,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return next();
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Audit logging middleware
|
|
54
|
-
*/
|
|
55
|
-
export function createAuditMiddleware(options: {
|
|
56
|
-
onLog: (entry: AuditLogEntry) => void | Promise<void>;
|
|
57
|
-
}): MCPMiddleware {
|
|
58
|
-
return async (ctx, next) => {
|
|
59
|
-
const result = await next();
|
|
60
|
-
|
|
61
|
-
const entry: AuditLogEntry = {
|
|
62
|
-
timestamp: new Date().toISOString(),
|
|
63
|
-
requestId: ctx.requestId,
|
|
64
|
-
toolName: ctx.toolName,
|
|
65
|
-
arguments: ctx.arguments,
|
|
66
|
-
success: result.success,
|
|
67
|
-
duration: result.duration,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
if (ctx.tenantId) entry.tenantId = ctx.tenantId;
|
|
71
|
-
if (ctx.userId) entry.userId = ctx.userId;
|
|
72
|
-
if (result.error) entry.error = result.error;
|
|
73
|
-
|
|
74
|
-
// Don't block on audit logging
|
|
75
|
-
Promise.resolve(options.onLog(entry)).catch(console.error);
|
|
76
|
-
|
|
77
|
-
return result;
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface AuditLogEntry {
|
|
82
|
-
timestamp: string;
|
|
83
|
-
requestId: string;
|
|
84
|
-
tenantId?: string;
|
|
85
|
-
userId?: string;
|
|
86
|
-
toolName: string;
|
|
87
|
-
arguments: Record<string, unknown>;
|
|
88
|
-
success: boolean;
|
|
89
|
-
duration: number;
|
|
90
|
-
error?: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Tool access control middleware
|
|
95
|
-
*/
|
|
96
|
-
export function createAccessControlMiddleware(options: {
|
|
97
|
-
blockedTools?: string[];
|
|
98
|
-
allowedTools?: string[];
|
|
99
|
-
}): MCPMiddleware {
|
|
100
|
-
return async (ctx, next) => {
|
|
101
|
-
// Check blocked list
|
|
102
|
-
if (options.blockedTools?.includes(ctx.toolName)) {
|
|
103
|
-
return {
|
|
104
|
-
success: false,
|
|
105
|
-
error: `Tool ${ctx.toolName} is blocked`,
|
|
106
|
-
duration: 0,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Check allowed list (if specified)
|
|
111
|
-
if (
|
|
112
|
-
options.allowedTools &&
|
|
113
|
-
options.allowedTools.length > 0 &&
|
|
114
|
-
!options.allowedTools.includes(ctx.toolName)
|
|
115
|
-
) {
|
|
116
|
-
return {
|
|
117
|
-
success: false,
|
|
118
|
-
error: `Tool ${ctx.toolName} is not in allowed list`,
|
|
119
|
-
duration: 0,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return next();
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Compose multiple middlewares
|
|
129
|
-
*/
|
|
130
|
-
export function composeMCPMiddleware(middlewares: MCPMiddleware[]): MCPMiddleware {
|
|
131
|
-
return async (ctx, finalNext) => {
|
|
132
|
-
let index = -1;
|
|
133
|
-
|
|
134
|
-
const dispatch = async (i: number): Promise<ToolExecutionResult> => {
|
|
135
|
-
if (i <= index) {
|
|
136
|
-
throw new Error("next() called multiple times");
|
|
137
|
-
}
|
|
138
|
-
index = i;
|
|
139
|
-
|
|
140
|
-
const middleware = middlewares[i];
|
|
141
|
-
if (!middleware) {
|
|
142
|
-
return finalNext();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return middleware(ctx, () => dispatch(i + 1));
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return dispatch(0);
|
|
149
|
-
};
|
|
150
|
-
}
|
package/src/registry/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { MCPServerRegistry, serverRegistry } from "./serverRegistry";
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import type { MCPContext, MCPServerConfig, ToolDefinition } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Registry for MCP servers and their tools
|
|
5
|
-
* Handles server registration, discovery, and access control
|
|
6
|
-
*/
|
|
7
|
-
export class MCPServerRegistry {
|
|
8
|
-
private servers: Map<string, MCPServerConfig> = new Map();
|
|
9
|
-
private toolIndex: Map<string, string> = new Map(); // tool name -> server id
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Register an MCP server
|
|
13
|
-
*/
|
|
14
|
-
register(config: MCPServerConfig): void {
|
|
15
|
-
this.servers.set(config.id, config);
|
|
16
|
-
|
|
17
|
-
// Index tools for quick lookup
|
|
18
|
-
for (const tool of config.tools) {
|
|
19
|
-
const qualifiedName = `${config.id}:${tool.name}`;
|
|
20
|
-
this.toolIndex.set(qualifiedName, config.id);
|
|
21
|
-
// Also allow short name if unique
|
|
22
|
-
if (!this.toolIndex.has(tool.name)) {
|
|
23
|
-
this.toolIndex.set(tool.name, config.id);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Unregister an MCP server
|
|
30
|
-
*/
|
|
31
|
-
unregister(serverId: string): boolean {
|
|
32
|
-
const server = this.servers.get(serverId);
|
|
33
|
-
if (!server) return false;
|
|
34
|
-
|
|
35
|
-
// Remove tool index entries
|
|
36
|
-
for (const tool of server.tools) {
|
|
37
|
-
const qualifiedName = `${serverId}:${tool.name}`;
|
|
38
|
-
this.toolIndex.delete(qualifiedName);
|
|
39
|
-
// Only remove short name if it points to this server
|
|
40
|
-
if (this.toolIndex.get(tool.name) === serverId) {
|
|
41
|
-
this.toolIndex.delete(tool.name);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
this.servers.delete(serverId);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get server by ID
|
|
51
|
-
*/
|
|
52
|
-
getServer(serverId: string): MCPServerConfig | undefined {
|
|
53
|
-
return this.servers.get(serverId);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Find server by tool name
|
|
58
|
-
*/
|
|
59
|
-
findServerByTool(toolName: string): MCPServerConfig | undefined {
|
|
60
|
-
const serverId = this.toolIndex.get(toolName);
|
|
61
|
-
if (!serverId) return undefined;
|
|
62
|
-
return this.servers.get(serverId);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get all registered servers
|
|
67
|
-
*/
|
|
68
|
-
getAllServers(): MCPServerConfig[] {
|
|
69
|
-
return Array.from(this.servers.values());
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get all available tools
|
|
74
|
-
*/
|
|
75
|
-
getAllTools(): Array<ToolDefinition & { serverId: string }> {
|
|
76
|
-
const tools: Array<ToolDefinition & { serverId: string }> = [];
|
|
77
|
-
for (const server of this.servers.values()) {
|
|
78
|
-
for (const tool of server.tools) {
|
|
79
|
-
tools.push({ ...tool, serverId: server.id });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return tools;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Check if a context has access to a server
|
|
87
|
-
*/
|
|
88
|
-
canAccess(serverId: string, context: MCPContext): boolean {
|
|
89
|
-
const server = this.servers.get(serverId);
|
|
90
|
-
if (!server) return false;
|
|
91
|
-
|
|
92
|
-
// Check plan restrictions
|
|
93
|
-
if (server.allowedPlans && server.allowedPlans.length > 0) {
|
|
94
|
-
if (!context.plan || !server.allowedPlans.includes(context.plan)) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Check tenant restrictions
|
|
100
|
-
if (server.allowedTenants && server.allowedTenants.length > 0) {
|
|
101
|
-
if (!context.tenantId || !server.allowedTenants.includes(context.tenantId)) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Check whether a context can execute a specific tool. Tool-level policy
|
|
111
|
-
* narrows the server policy and supports tenant, plan, and permission gates.
|
|
112
|
-
*/
|
|
113
|
-
canAccessTool(toolName: string, context: MCPContext): boolean {
|
|
114
|
-
const server = this.findServerByTool(toolName);
|
|
115
|
-
if (!server || !this.canAccess(server.id, context)) return false;
|
|
116
|
-
|
|
117
|
-
const tool = this.findTool(server, toolName);
|
|
118
|
-
if (!tool) return false;
|
|
119
|
-
|
|
120
|
-
if (tool.allowedPlans && tool.allowedPlans.length > 0) {
|
|
121
|
-
if (!context.plan || !tool.allowedPlans.includes(context.plan)) return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (tool.allowedTenants && tool.allowedTenants.length > 0) {
|
|
125
|
-
if (!context.tenantId || !tool.allowedTenants.includes(context.tenantId)) return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (tool.requiredPermissions && tool.requiredPermissions.length > 0) {
|
|
129
|
-
const permissions = new Set(context.permissions ?? []);
|
|
130
|
-
if (!tool.requiredPermissions.every((permission) => permissions.has(permission))) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get tools accessible by a context
|
|
140
|
-
*/
|
|
141
|
-
getAccessibleTools(context: MCPContext): Array<ToolDefinition & { serverId: string }> {
|
|
142
|
-
const tools: Array<ToolDefinition & { serverId: string }> = [];
|
|
143
|
-
|
|
144
|
-
for (const server of this.servers.values()) {
|
|
145
|
-
if (this.canAccess(server.id, context)) {
|
|
146
|
-
for (const tool of server.tools) {
|
|
147
|
-
if (this.canAccessTool(`${server.id}:${tool.name}`, context)) {
|
|
148
|
-
tools.push({ ...tool, serverId: server.id });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return tools;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private findTool(server: MCPServerConfig, toolName: string): ToolDefinition | undefined {
|
|
158
|
-
return server.tools.find(
|
|
159
|
-
(tool) => tool.name === toolName || `${server.id}:${tool.name}` === toolName,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Global registry instance
|
|
165
|
-
export const serverRegistry = new MCPServerRegistry();
|