@nebutra/mcp 0.1.0
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/AGENTS.md +62 -0
- package/LICENSE +676 -0
- package/README.md +123 -0
- package/bin/nebutra-mcp.js +5 -0
- package/dist/server/contextServer.js +1 -0
- package/package.json +57 -0
- package/src/client/index.ts +1 -0
- package/src/client/mcpClient.ts +172 -0
- package/src/index.ts +23 -0
- package/src/middleware/index.ts +10 -0
- package/src/middleware/mcpMiddleware.ts +150 -0
- package/src/registry/index.ts +1 -0
- package/src/registry/serverRegistry.ts +128 -0
- package/src/server/contextServer.ts +138 -0
- package/src/server/index.ts +5 -0
- package/src/server/internalServers.ts +70 -0
- package/src/types.ts +118 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @nebutra/mcp
|
|
2
|
+
|
|
3
|
+
> **Status: WIP** — Not yet integrated into any production app. Do not import until this notice is removed.
|
|
4
|
+
|
|
5
|
+
Model Context Protocol (MCP) for AI agent tool calling.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @nebutra/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
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
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Agent → MCPClient → ServerRegistry → MCP Servers
|
|
24
|
+
↓
|
|
25
|
+
Middleware (rate-limit, audit, access-control)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Initialize
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { mcpClient, registerInternalServers } from "@nebutra/mcp";
|
|
34
|
+
|
|
35
|
+
// Register built-in servers
|
|
36
|
+
registerInternalServers();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Execute Tools
|
|
40
|
+
|
|
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
|
+
```
|
|
73
|
+
|
|
74
|
+
## Register Custom Server
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { serverRegistry } from "@nebutra/mcp";
|
|
78
|
+
|
|
79
|
+
serverRegistry.register({
|
|
80
|
+
name: "custom-tools",
|
|
81
|
+
url: "http://localhost:3001/mcp",
|
|
82
|
+
tools: [
|
|
83
|
+
{
|
|
84
|
+
name: "custom_tool",
|
|
85
|
+
description: "My custom tool",
|
|
86
|
+
parameters: { type: "object", properties: {} },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
allowedPlans: ["PRO", "ENTERPRISE"],
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Middleware
|
|
94
|
+
|
|
95
|
+
### Rate Limiting
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Configured per tool
|
|
99
|
+
const limits = {
|
|
100
|
+
generate_text: { requests: 100, window: "1m" },
|
|
101
|
+
embed_text: { requests: 1000, window: "1h" },
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Audit Logging
|
|
106
|
+
|
|
107
|
+
All tool executions are logged:
|
|
108
|
+
|
|
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
|
+
```
|
|
119
|
+
|
|
120
|
+
## Related
|
|
121
|
+
|
|
122
|
+
- [AI service](../../backends/python/ai/)
|
|
123
|
+
- [API Gateway](../../backends/gateway/)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "AGPL-3.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"nebutra": {
|
|
8
|
+
"status": "wip",
|
|
9
|
+
"productionReady": false,
|
|
10
|
+
"requires": [
|
|
11
|
+
"@modelcontextprotocol/sdk runtime"
|
|
12
|
+
],
|
|
13
|
+
"gaps": [
|
|
14
|
+
"No production app integrations",
|
|
15
|
+
"Context server binary is a placeholder stub",
|
|
16
|
+
"Tool registry + plan-based access control pending"
|
|
17
|
+
],
|
|
18
|
+
"featureId": "mcp",
|
|
19
|
+
"category": "ai",
|
|
20
|
+
"summary": "MCP server primitives (Model Context Protocol)"
|
|
21
|
+
},
|
|
22
|
+
"main": "./src/index.ts",
|
|
23
|
+
"types": "./src/index.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./client": "./src/client/index.ts",
|
|
27
|
+
"./server": "./src/server/index.ts",
|
|
28
|
+
"./middleware": "./src/middleware/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"nebutra-mcp": "./dist/server/contextServer.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
35
|
+
"zod": "^4.3.6"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/mcp#readme",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
44
|
+
"directory": "packages/ai/mcp"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MCPClient, mcpClient } from "./mcpClient";
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { serverRegistry } from "../registry/serverRegistry";
|
|
2
|
+
import type { MCPContext, MCPServerConfig, ToolExecutionResult } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP Client for executing tools across registered servers
|
|
6
|
+
*/
|
|
7
|
+
export class MCPClient {
|
|
8
|
+
private requestCounter = 0;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute a tool by name
|
|
12
|
+
*/
|
|
13
|
+
async executeTool(
|
|
14
|
+
toolName: string,
|
|
15
|
+
args: Record<string, unknown>,
|
|
16
|
+
context: MCPContext,
|
|
17
|
+
): Promise<ToolExecutionResult> {
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
|
|
20
|
+
// Find server for this tool
|
|
21
|
+
const server = serverRegistry.findServerByTool(toolName);
|
|
22
|
+
if (!server) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
error: `Tool not found: ${toolName}`,
|
|
26
|
+
duration: Date.now() - startTime,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check access
|
|
31
|
+
if (!serverRegistry.canAccess(server.id, context)) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: `Access denied to tool: ${toolName}`,
|
|
35
|
+
duration: Date.now() - startTime,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Execute based on transport
|
|
40
|
+
try {
|
|
41
|
+
const result = await this.executeOnServer(server, toolName, args, context);
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
result,
|
|
45
|
+
duration: Date.now() - startTime,
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: error instanceof Error ? error.message : String(error),
|
|
51
|
+
duration: Date.now() - startTime,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute tool on a specific server
|
|
58
|
+
*/
|
|
59
|
+
private async executeOnServer(
|
|
60
|
+
server: MCPServerConfig,
|
|
61
|
+
toolName: string,
|
|
62
|
+
args: Record<string, unknown>,
|
|
63
|
+
context: MCPContext,
|
|
64
|
+
): Promise<unknown> {
|
|
65
|
+
const requestId = `${context.requestId}-${++this.requestCounter}`;
|
|
66
|
+
|
|
67
|
+
switch (server.transport) {
|
|
68
|
+
case "http":
|
|
69
|
+
return this.executeHttp(server, toolName, args, requestId);
|
|
70
|
+
case "websocket":
|
|
71
|
+
return this.executeWebSocket(server, toolName, args, requestId);
|
|
72
|
+
case "stdio":
|
|
73
|
+
throw new Error("stdio transport not supported in browser/serverless");
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`Unknown transport: ${server.transport}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Execute via HTTP transport
|
|
81
|
+
*/
|
|
82
|
+
private async executeHttp(
|
|
83
|
+
server: MCPServerConfig,
|
|
84
|
+
toolName: string,
|
|
85
|
+
args: Record<string, unknown>,
|
|
86
|
+
requestId: string,
|
|
87
|
+
): Promise<unknown> {
|
|
88
|
+
const headers: Record<string, string> = {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
"X-Request-ID": requestId,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Add authentication if configured
|
|
94
|
+
if (server.authentication?.type === "bearer") {
|
|
95
|
+
// In production, get token from secure storage
|
|
96
|
+
const token = process.env[`MCP_${server.id.toUpperCase()}_TOKEN`];
|
|
97
|
+
if (token) {
|
|
98
|
+
headers.Authorization = `Bearer ${token}`;
|
|
99
|
+
}
|
|
100
|
+
} else if (server.authentication?.type === "api-key") {
|
|
101
|
+
const apiKey = process.env[`MCP_${server.id.toUpperCase()}_KEY`];
|
|
102
|
+
const headerName = server.authentication.headerName || "X-API-Key";
|
|
103
|
+
if (apiKey) {
|
|
104
|
+
headers[headerName] = apiKey;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = await fetch(server.endpoint, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
jsonrpc: "2.0",
|
|
113
|
+
id: requestId,
|
|
114
|
+
method: "tools/call",
|
|
115
|
+
params: {
|
|
116
|
+
name: toolName,
|
|
117
|
+
arguments: args,
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const data = await response.json();
|
|
127
|
+
|
|
128
|
+
if (data.error) {
|
|
129
|
+
throw new Error(data.error.message || "Unknown MCP error");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return data.result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Execute via WebSocket transport
|
|
137
|
+
*/
|
|
138
|
+
private async executeWebSocket(
|
|
139
|
+
_server: MCPServerConfig,
|
|
140
|
+
_toolName: string,
|
|
141
|
+
_args: Record<string, unknown>,
|
|
142
|
+
_requestId: string,
|
|
143
|
+
): Promise<unknown> {
|
|
144
|
+
// WebSocket implementation would maintain persistent connections
|
|
145
|
+
// For now, throw not implemented
|
|
146
|
+
throw new Error("WebSocket transport not yet implemented");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List available tools for a context
|
|
151
|
+
*/
|
|
152
|
+
listTools(context: MCPContext) {
|
|
153
|
+
return serverRegistry.getAccessibleTools(context);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get tool definition
|
|
158
|
+
*/
|
|
159
|
+
getTool(toolName: string, context: MCPContext) {
|
|
160
|
+
const server = serverRegistry.findServerByTool(toolName);
|
|
161
|
+
if (!server) return undefined;
|
|
162
|
+
|
|
163
|
+
if (!serverRegistry.canAccess(server.id, context)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return server.tools.find((t) => t.name === toolName || `${server.id}:${t.name}` === toolName);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Global client instance
|
|
172
|
+
export const mcpClient = new MCPClient();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
|
|
3
|
+
// Client
|
|
4
|
+
export { MCPClient, mcpClient } from "./client/index";
|
|
5
|
+
// Middleware
|
|
6
|
+
export {
|
|
7
|
+
type AuditLogEntry,
|
|
8
|
+
composeMCPMiddleware,
|
|
9
|
+
createAccessControlMiddleware,
|
|
10
|
+
createAuditMiddleware,
|
|
11
|
+
createRateLimitMiddleware,
|
|
12
|
+
type MCPMiddleware,
|
|
13
|
+
type MCPMiddlewareContext,
|
|
14
|
+
} from "./middleware/index";
|
|
15
|
+
// Registry
|
|
16
|
+
export { MCPServerRegistry, serverRegistry } from "./registry/index";
|
|
17
|
+
// Server
|
|
18
|
+
export {
|
|
19
|
+
getInternalServerIds,
|
|
20
|
+
INTERNAL_SERVERS,
|
|
21
|
+
registerInternalServers,
|
|
22
|
+
} from "./server/index";
|
|
23
|
+
export * from "./types";
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MCPServerRegistry, serverRegistry } from "./serverRegistry";
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
* Get tools accessible by a context
|
|
111
|
+
*/
|
|
112
|
+
getAccessibleTools(context: MCPContext): Array<ToolDefinition & { serverId: string }> {
|
|
113
|
+
const tools: Array<ToolDefinition & { serverId: string }> = [];
|
|
114
|
+
|
|
115
|
+
for (const server of this.servers.values()) {
|
|
116
|
+
if (this.canAccess(server.id, context)) {
|
|
117
|
+
for (const tool of server.tools) {
|
|
118
|
+
tools.push({ ...tool, serverId: server.id });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tools;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Global registry instance
|
|
128
|
+
export const serverRegistry = new MCPServerRegistry();
|