@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/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,5 @@
1
+ #!/usr/bin/env node
2
+ import { startContextServer } from "../src/server/contextServer.js";
3
+
4
+ // Starts the stdio MCP server for Cursor / Windsurf.
5
+ startContextServer();
@@ -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,10 @@
1
+ export {
2
+ type AuditLogEntry,
3
+ composeMCPMiddleware,
4
+ createAccessControlMiddleware,
5
+ createAuditMiddleware,
6
+ createRateLimitMiddleware,
7
+ type MCPMiddleware,
8
+ type MCPMiddlewareContext,
9
+ type MCPMiddlewareNext,
10
+ } from "./mcpMiddleware";
@@ -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();