@nebutra/mcp 0.1.3 → 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.
@@ -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
- }
@@ -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();
@@ -1,211 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import {
8
- CallToolRequestSchema,
9
- ListResourcesRequestSchema,
10
- ListToolsRequestSchema,
11
- ReadResourceRequestSchema,
12
- } from "@modelcontextprotocol/sdk/types.js";
13
-
14
- /**
15
- * Nebutra MCP Context Server
16
- * Exposes core project files and state to Cursor / Windsurf allowing AI Agents
17
- * to instantly understand the Nebutra-Sailor application structure and routing logic.
18
- */
19
- class NebutraContextServer {
20
- private server: Server;
21
- private projectRoot: string;
22
-
23
- constructor(projectRoot?: string) {
24
- this.projectRoot = projectRoot || process.cwd();
25
-
26
- this.server = new Server(
27
- {
28
- name: "@nebutra/context-server",
29
- version: "0.1.0",
30
- },
31
- {
32
- capabilities: {
33
- resources: {},
34
- tools: {},
35
- },
36
- },
37
- );
38
-
39
- this.setupResourceHandlers();
40
- this.setupToolHandlers();
41
-
42
- this.server.onerror = (_error) => {};
43
- }
44
-
45
- private setupResourceHandlers() {
46
- const handlers = createContextServerHandlers(this.projectRoot);
47
-
48
- this.server.setRequestHandler(ListResourcesRequestSchema, async () => handlers.listResources());
49
-
50
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
51
- return handlers.readResource(request.params);
52
- });
53
- }
54
-
55
- private setupToolHandlers() {
56
- const handlers = createContextServerHandlers(this.projectRoot);
57
-
58
- this.server.setRequestHandler(ListToolsRequestSchema, async () => handlers.listTools());
59
-
60
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
61
- return handlers.callTool(request.params);
62
- });
63
- }
64
-
65
- async run() {
66
- const transport = new StdioServerTransport();
67
- await this.server.connect(transport);
68
- }
69
- }
70
-
71
- const RESOURCE_MAP = {
72
- "file:///nebutra.config.json": {
73
- relativePath: "nebutra.config.json",
74
- name: "Nebutra Configuration",
75
- mimeType: "application/json",
76
- description:
77
- "Global configurations for the Nebutra template including enabled features (auth, payments, i18n, db).",
78
- },
79
- "file:///prisma/schema.prisma": {
80
- relativePath: "packages/platform/db/prisma/schema.prisma",
81
- name: "Database Schema Overview",
82
- mimeType: "text/plain",
83
- description: "The core Prisma schema defining the database tables and relationships.",
84
- },
85
- } as const;
86
-
87
- export function createContextServerHandlers(projectRoot: string) {
88
- return {
89
- async listResources() {
90
- return {
91
- resources: Object.entries(RESOURCE_MAP).map(([uri, resource]) => ({
92
- uri,
93
- name: resource.name,
94
- mimeType: resource.mimeType,
95
- description: resource.description,
96
- })),
97
- };
98
- },
99
-
100
- async readResource({ uri }: { uri: string }) {
101
- const resource = RESOURCE_MAP[uri as keyof typeof RESOURCE_MAP];
102
- if (!resource) {
103
- throw new Error(`Resource not found: ${uri}`);
104
- }
105
-
106
- const filePath = path.join(projectRoot, resource.relativePath);
107
- try {
108
- const content = fs.readFileSync(filePath, "utf-8");
109
- return {
110
- contents: [
111
- {
112
- uri,
113
- mimeType: resource.mimeType,
114
- text: content,
115
- },
116
- ],
117
- };
118
- } catch {
119
- throw new Error(`Failed to read the requested resource file at ${filePath}`);
120
- }
121
- },
122
-
123
- async listTools() {
124
- return {
125
- tools: [
126
- {
127
- name: "get_project_structure",
128
- description: "Returns a bounded app/package routing tree for the current project.",
129
- inputSchema: {
130
- type: "object",
131
- properties: {},
132
- required: [],
133
- },
134
- },
135
- ],
136
- };
137
- },
138
-
139
- async callTool({ name }: { name: string; arguments?: Record<string, unknown> | undefined }) {
140
- if (name !== "get_project_structure") {
141
- throw new Error(`Unknown tool: ${name}`);
142
- }
143
-
144
- try {
145
- return {
146
- content: [
147
- {
148
- type: "text",
149
- text: getProjectStructure(projectRoot),
150
- },
151
- ],
152
- };
153
- } catch (error) {
154
- return {
155
- content: [
156
- {
157
- type: "text",
158
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
159
- },
160
- ],
161
- isError: true,
162
- };
163
- }
164
- },
165
- };
166
- }
167
-
168
- function getProjectStructure(projectRoot: string): string {
169
- const roots = ["apps", "packages", "backends"].filter((segment) =>
170
- fs.existsSync(path.join(projectRoot, segment)),
171
- );
172
- if (roots.length === 0) return "No Nebutra workspace roots found.";
173
-
174
- const entries: string[] = [];
175
- for (const root of roots) {
176
- walkProjectTree(path.join(projectRoot, root), root, entries, 4);
177
- }
178
- return entries.length > 0 ? entries.join("\n") : "No project entries found.";
179
- }
180
-
181
- function walkProjectTree(
182
- absoluteDir: string,
183
- relativeDir: string,
184
- entries: string[],
185
- depthRemaining: number,
186
- ): void {
187
- if (depthRemaining < 0 || entries.length >= 200) return;
188
-
189
- entries.push(relativeDir);
190
-
191
- for (const dirent of fs.readdirSync(absoluteDir, { withFileTypes: true })) {
192
- if (!dirent.isDirectory()) continue;
193
- if (dirent.name === "node_modules" || dirent.name.startsWith(".")) continue;
194
- walkProjectTree(
195
- path.join(absoluteDir, dirent.name),
196
- path.join(relativeDir, dirent.name),
197
- entries,
198
- depthRemaining - 1,
199
- );
200
- }
201
- }
202
-
203
- // Export a factory or run directly if invoked as script
204
- export const startContextServer = () => {
205
- const server = new NebutraContextServer();
206
- server.run().catch(console.error);
207
- };
208
-
209
- if (process.argv[1]?.endsWith("contextServer.js") || process.argv[1]?.endsWith("nebutra-mcp")) {
210
- startContextServer();
211
- }
@@ -1,6 +0,0 @@
1
- export {
2
- getInternalServerIds,
3
- INTERNAL_SERVERS,
4
- registerInternalServers,
5
- } from "./internalServers";
6
- export { createUiAgentMcpServer } from "./uiAgentServer";
@@ -1,72 +0,0 @@
1
- import { serverRegistry } from "../registry/serverRegistry";
2
- import type { MCPServerConfig } from "../types";
3
- import { createUiAgentMcpServer } from "./uiAgentServer";
4
-
5
- /**
6
- * Register internal Nebutra services as MCP servers
7
- * These wrap our existing microservices with MCP-compatible interfaces
8
- */
9
-
10
- export const INTERNAL_SERVERS: MCPServerConfig[] = [
11
- createUiAgentMcpServer(),
12
- // AI Service
13
- {
14
- id: "nebutra-ai",
15
- name: "Nebutra AI Service",
16
- description: "AI-powered text generation, embedding, and translation",
17
- endpoint: process.env.AI_SERVICE_URL || "http://localhost:8001",
18
- transport: "http",
19
- tools: [
20
- {
21
- name: "generate_text",
22
- description: "Generate text using LLM",
23
- parameters: {
24
- prompt: { type: "string", description: "Input prompt", required: true },
25
- max_tokens: { type: "number", description: "Max tokens to generate", default: 1000 },
26
- temperature: { type: "number", description: "Sampling temperature", default: 0.7 },
27
- },
28
- returns: "Generated text",
29
- },
30
- {
31
- name: "create_embedding",
32
- description: "Create vector embedding for text",
33
- parameters: {
34
- text: { type: "string", description: "Text to embed", required: true },
35
- model: {
36
- type: "string",
37
- description: "Embedding model",
38
- default: "text-embedding-3-small",
39
- },
40
- },
41
- returns: "Vector embedding array",
42
- },
43
- {
44
- name: "translate",
45
- description: "Translate text between languages",
46
- parameters: {
47
- text: { type: "string", description: "Text to translate", required: true },
48
- source: { type: "string", description: "Source language code", default: "en" },
49
- target: { type: "string", description: "Target language code", required: true },
50
- },
51
- returns: "Translated text",
52
- },
53
- ],
54
- rateLimit: { maxRequests: 100, windowMs: 60000 },
55
- },
56
- ];
57
-
58
- /**
59
- * Register all internal servers
60
- */
61
- export function registerInternalServers(): void {
62
- for (const server of INTERNAL_SERVERS) {
63
- serverRegistry.register(server);
64
- }
65
- }
66
-
67
- /**
68
- * Get list of internal server IDs
69
- */
70
- export function getInternalServerIds(): string[] {
71
- return INTERNAL_SERVERS.map((s) => s.id);
72
- }