@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
package/src/host.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
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
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// Client
|
|
4
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";
|
|
5
12
|
// Middleware
|
|
6
13
|
export {
|
|
7
14
|
type AuditLogEntry,
|
|
@@ -106,6 +106,35 @@ export class MCPServerRegistry {
|
|
|
106
106
|
return true;
|
|
107
107
|
}
|
|
108
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
|
+
|
|
109
138
|
/**
|
|
110
139
|
* Get tools accessible by a context
|
|
111
140
|
*/
|
|
@@ -115,13 +144,21 @@ export class MCPServerRegistry {
|
|
|
115
144
|
for (const server of this.servers.values()) {
|
|
116
145
|
if (this.canAccess(server.id, context)) {
|
|
117
146
|
for (const tool of server.tools) {
|
|
118
|
-
|
|
147
|
+
if (this.canAccessTool(`${server.id}:${tool.name}`, context)) {
|
|
148
|
+
tools.push({ ...tool, serverId: server.id });
|
|
149
|
+
}
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
return tools;
|
|
124
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
|
+
}
|
|
125
162
|
}
|
|
126
163
|
|
|
127
164
|
// Global registry instance
|
|
@@ -43,43 +43,74 @@ class NebutraContextServer {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
private setupResourceHandlers() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
uri: "file:///nebutra.config.json",
|
|
50
|
-
name: "Nebutra Configuration",
|
|
51
|
-
mimeType: "application/json",
|
|
52
|
-
description:
|
|
53
|
-
"Global configurations for the Nebutra template including enabled features (auth, payments, i18n, db).",
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
uri: "file:///prisma/schema.prisma",
|
|
57
|
-
name: "Database Schema Overview",
|
|
58
|
-
mimeType: "text/plain",
|
|
59
|
-
description: "The core Prisma schema defining the database tables and relationships.",
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
}));
|
|
46
|
+
const handlers = createContextServerHandlers(this.projectRoot);
|
|
47
|
+
|
|
48
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => handlers.listResources());
|
|
63
49
|
|
|
64
50
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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) {
|
|
73
103
|
throw new Error(`Resource not found: ${uri}`);
|
|
74
104
|
}
|
|
75
105
|
|
|
106
|
+
const filePath = path.join(projectRoot, resource.relativePath);
|
|
76
107
|
try {
|
|
77
108
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
78
109
|
return {
|
|
79
110
|
contents: [
|
|
80
111
|
{
|
|
81
112
|
uri,
|
|
82
|
-
mimeType:
|
|
113
|
+
mimeType: resource.mimeType,
|
|
83
114
|
text: content,
|
|
84
115
|
},
|
|
85
116
|
],
|
|
@@ -87,43 +118,85 @@ class NebutraContextServer {
|
|
|
87
118
|
} catch {
|
|
88
119
|
throw new Error(`Failed to read the requested resource file at ${filePath}`);
|
|
89
120
|
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
121
|
+
},
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
},
|
|
103
134
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
},
|
|
107
138
|
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const appsDir = path.join(this.projectRoot, "apps");
|
|
112
|
-
if (!fs.existsSync(appsDir))
|
|
113
|
-
return { content: [{ type: "text", text: "Apps dir not found." }] };
|
|
114
|
-
const dirs = fs.readdirSync(appsDir);
|
|
115
|
-
return { content: [{ type: "text", text: `Apps: ${dirs.join(", ")}` }] };
|
|
116
|
-
} catch (error) {
|
|
117
|
-
return { content: [{ type: "text", text: `Error: ${error}` }], isError: true };
|
|
118
|
-
}
|
|
139
|
+
async callTool({ name }: { name: string; arguments?: Record<string, unknown> | undefined }) {
|
|
140
|
+
if (name !== "get_project_structure") {
|
|
141
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
119
142
|
}
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
177
|
}
|
|
178
|
+
return entries.length > 0 ? entries.join("\n") : "No project entries found.";
|
|
179
|
+
}
|
|
123
180
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
);
|
|
127
200
|
}
|
|
128
201
|
}
|
|
129
202
|
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,9 @@ export const ToolDefinitionSchema = z.object({
|
|
|
16
16
|
description: z.string(),
|
|
17
17
|
parameters: z.record(z.string(), ToolParameterSchema).optional(),
|
|
18
18
|
returns: z.string().optional(),
|
|
19
|
+
allowedPlans: z.array(z.string()).optional(),
|
|
20
|
+
allowedTenants: z.array(z.string()).optional(),
|
|
21
|
+
requiredPermissions: z.array(z.string()).optional(),
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
export type ToolParameter = z.infer<typeof ToolParameterSchema>;
|
|
@@ -74,6 +77,13 @@ export interface ToolExecutionResult {
|
|
|
74
77
|
tokensUsed?: number;
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
export interface MCPServerManifest {
|
|
81
|
+
name: string;
|
|
82
|
+
version: string;
|
|
83
|
+
scopes: string[];
|
|
84
|
+
homepage?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
// ============================================
|
|
78
88
|
// MCP Server Registration
|
|
79
89
|
// ============================================
|
|
@@ -83,8 +93,10 @@ export interface MCPServerConfig {
|
|
|
83
93
|
name: string;
|
|
84
94
|
description: string;
|
|
85
95
|
endpoint: string;
|
|
86
|
-
transport: "http" | "websocket" | "stdio";
|
|
96
|
+
transport: "http" | "websocket" | "stdio" | "local";
|
|
97
|
+
manifest?: MCPServerManifest;
|
|
87
98
|
tools: ToolDefinition[];
|
|
99
|
+
handlers?: Record<string, MCPToolHandler>;
|
|
88
100
|
authentication?: {
|
|
89
101
|
type: "bearer" | "api-key" | "none";
|
|
90
102
|
headerName?: string;
|
|
@@ -98,6 +110,11 @@ export interface MCPServerConfig {
|
|
|
98
110
|
allowedTenants?: string[]; // specific tenant IDs
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
export type MCPToolHandler = (
|
|
114
|
+
args: Record<string, unknown>,
|
|
115
|
+
context: MCPContext,
|
|
116
|
+
) => unknown | Promise<unknown>;
|
|
117
|
+
|
|
101
118
|
// ============================================
|
|
102
119
|
// Smithery Integration
|
|
103
120
|
// ============================================
|
package/tsup.config.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { defineConfig } from "tsup";
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
entry: ["src/index.ts", "src/server/contextServer.ts"],
|
|
4
|
+
entry: ["src/index.ts", "src/cli.ts", "src/server/contextServer.ts"],
|
|
5
5
|
format: ["esm"],
|
|
6
6
|
dts: true,
|
|
7
7
|
clean: true,
|
|
8
8
|
target: "node18",
|
|
9
9
|
outDir: "dist",
|
|
10
10
|
minify: false,
|
|
11
|
+
external: [
|
|
12
|
+
"@modelcontextprotocol/sdk",
|
|
13
|
+
"@nebutra/errors",
|
|
14
|
+
"@nebutra/event-log",
|
|
15
|
+
"@nebutra/trace-store",
|
|
16
|
+
],
|
|
11
17
|
});
|