@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.
- package/package.json +21 -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 -295
- package/src/cli.ts +0 -47
- package/src/client/index.ts +0 -1
- package/src/client/mcpClient.ts +0 -206
- 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 -6
- package/src/server/internalServers.ts +0 -72
- package/src/server/uiAgentServer.ts +0 -167
- package/src/types.ts +0 -135
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/tsup.config.ts +0 -17
package/src/client/mcpClient.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
-
import type { MCPServerRegistry } from "../registry/serverRegistry";
|
|
4
|
-
import { serverRegistry } from "../registry/serverRegistry";
|
|
5
|
-
import type { MCPContext, MCPServerConfig, ToolExecutionResult } from "../types";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* MCP Client for executing tools across registered servers
|
|
9
|
-
*/
|
|
10
|
-
export class MCPClient {
|
|
11
|
-
private requestCounter = 0;
|
|
12
|
-
/** One SDK client per HTTP server id, reused across calls. */
|
|
13
|
-
private readonly httpClients = new Map<string, Client>();
|
|
14
|
-
|
|
15
|
-
constructor(private readonly registry: MCPServerRegistry = serverRegistry) {}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Execute a tool by name
|
|
19
|
-
*/
|
|
20
|
-
async executeTool(
|
|
21
|
-
toolName: string,
|
|
22
|
-
args: Record<string, unknown>,
|
|
23
|
-
context: MCPContext,
|
|
24
|
-
): Promise<ToolExecutionResult> {
|
|
25
|
-
const startTime = Date.now();
|
|
26
|
-
|
|
27
|
-
// Find server for this tool
|
|
28
|
-
const server = this.registry.findServerByTool(toolName);
|
|
29
|
-
if (!server) {
|
|
30
|
-
return {
|
|
31
|
-
success: false,
|
|
32
|
-
error: `Tool not found: ${toolName}`,
|
|
33
|
-
duration: Date.now() - startTime,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check access
|
|
38
|
-
if (!this.registry.canAccessTool(toolName, context)) {
|
|
39
|
-
return {
|
|
40
|
-
success: false,
|
|
41
|
-
error: `Access denied to tool: ${toolName}`,
|
|
42
|
-
duration: Date.now() - startTime,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Execute based on transport
|
|
47
|
-
try {
|
|
48
|
-
const result = await this.executeOnServer(server, toolName, args, context);
|
|
49
|
-
return {
|
|
50
|
-
success: true,
|
|
51
|
-
result,
|
|
52
|
-
duration: Date.now() - startTime,
|
|
53
|
-
};
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return {
|
|
56
|
-
success: false,
|
|
57
|
-
error: error instanceof Error ? error.message : String(error),
|
|
58
|
-
duration: Date.now() - startTime,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Execute tool on a specific server
|
|
65
|
-
*/
|
|
66
|
-
private async executeOnServer(
|
|
67
|
-
server: MCPServerConfig,
|
|
68
|
-
toolName: string,
|
|
69
|
-
args: Record<string, unknown>,
|
|
70
|
-
context: MCPContext,
|
|
71
|
-
): Promise<unknown> {
|
|
72
|
-
const requestId = `${context.requestId}-${++this.requestCounter}`;
|
|
73
|
-
|
|
74
|
-
switch (server.transport) {
|
|
75
|
-
case "http":
|
|
76
|
-
return this.executeHttp(server, toolName, args, requestId);
|
|
77
|
-
case "websocket":
|
|
78
|
-
return this.executeWebSocket(server, toolName, args, requestId);
|
|
79
|
-
case "local":
|
|
80
|
-
return this.executeLocal(server, toolName, args, context);
|
|
81
|
-
case "stdio":
|
|
82
|
-
throw new Error("stdio transport not supported in browser/serverless");
|
|
83
|
-
default:
|
|
84
|
-
throw new Error(`Unknown transport: ${server.transport}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Execute via HTTP transport, using the official MCP SDK's
|
|
90
|
-
* StreamableHTTPClientTransport + Client to perform the `tools/call`.
|
|
91
|
-
*/
|
|
92
|
-
private async executeHttp(
|
|
93
|
-
server: MCPServerConfig,
|
|
94
|
-
toolName: string,
|
|
95
|
-
args: Record<string, unknown>,
|
|
96
|
-
requestId: string,
|
|
97
|
-
): Promise<unknown> {
|
|
98
|
-
const client = await this.getHttpClient(server, requestId);
|
|
99
|
-
const shortName = toolName.startsWith(`${server.id}:`)
|
|
100
|
-
? toolName.slice(server.id.length + 1)
|
|
101
|
-
: toolName;
|
|
102
|
-
// SDK throws an McpError on JSON-RPC errors and a StreamableHTTPError on
|
|
103
|
-
// non-2xx responses; both propagate to executeTool's try/catch wrapper.
|
|
104
|
-
return client.callTool({ name: shortName, arguments: args });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Lazily create and cache an SDK Client connected over StreamableHTTP for a
|
|
109
|
-
* given server. Authentication headers are derived from the same env vars
|
|
110
|
-
* the previous hand-written transport used.
|
|
111
|
-
*/
|
|
112
|
-
private async getHttpClient(server: MCPServerConfig, requestId: string): Promise<Client> {
|
|
113
|
-
const cached = this.httpClients.get(server.id);
|
|
114
|
-
if (cached) return cached;
|
|
115
|
-
|
|
116
|
-
const headers: Record<string, string> = {
|
|
117
|
-
"X-Request-ID": requestId,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Add authentication if configured (same env-var contract as before).
|
|
121
|
-
if (server.authentication?.type === "bearer") {
|
|
122
|
-
const token = process.env[`MCP_${server.id.toUpperCase()}_TOKEN`];
|
|
123
|
-
if (token) {
|
|
124
|
-
headers.Authorization = `Bearer ${token}`;
|
|
125
|
-
}
|
|
126
|
-
} else if (server.authentication?.type === "api-key") {
|
|
127
|
-
const apiKey = process.env[`MCP_${server.id.toUpperCase()}_KEY`];
|
|
128
|
-
const headerName = server.authentication.headerName || "X-API-Key";
|
|
129
|
-
if (apiKey) {
|
|
130
|
-
headers[headerName] = apiKey;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const transport = new StreamableHTTPClientTransport(new URL(server.endpoint), {
|
|
135
|
-
requestInit: { headers },
|
|
136
|
-
});
|
|
137
|
-
const client = new Client({ name: "nebutra-mcp-client", version: "0.1.0" });
|
|
138
|
-
// Cast mirrors host.ts: the SDK's Transport type is incompatible with the
|
|
139
|
-
// repo's exactOptionalPropertyTypes (sessionId?: string).
|
|
140
|
-
await client.connect(transport as Parameters<Client["connect"]>[0]);
|
|
141
|
-
this.httpClients.set(server.id, client);
|
|
142
|
-
return client;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Close all cached SDK HTTP clients and clear the cache.
|
|
147
|
-
*/
|
|
148
|
-
async close(): Promise<void> {
|
|
149
|
-
await Promise.all(Array.from(this.httpClients.values()).map((client) => client.close()));
|
|
150
|
-
this.httpClients.clear();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Execute via WebSocket transport
|
|
155
|
-
*/
|
|
156
|
-
private async executeWebSocket(
|
|
157
|
-
_server: MCPServerConfig,
|
|
158
|
-
_toolName: string,
|
|
159
|
-
_args: Record<string, unknown>,
|
|
160
|
-
_requestId: string,
|
|
161
|
-
): Promise<unknown> {
|
|
162
|
-
// WebSocket implementation would maintain persistent connections
|
|
163
|
-
// For now, throw not implemented
|
|
164
|
-
throw new Error("WebSocket transport not yet implemented");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private async executeLocal(
|
|
168
|
-
server: MCPServerConfig,
|
|
169
|
-
toolName: string,
|
|
170
|
-
args: Record<string, unknown>,
|
|
171
|
-
context: MCPContext,
|
|
172
|
-
): Promise<unknown> {
|
|
173
|
-
const shortName = toolName.startsWith(`${server.id}:`)
|
|
174
|
-
? toolName.slice(server.id.length + 1)
|
|
175
|
-
: toolName;
|
|
176
|
-
const handler = server.handlers?.[shortName] ?? server.handlers?.[toolName];
|
|
177
|
-
if (!handler) {
|
|
178
|
-
throw new Error(`No local handler registered for tool: ${toolName}`);
|
|
179
|
-
}
|
|
180
|
-
return handler(args, context);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* List available tools for a context
|
|
185
|
-
*/
|
|
186
|
-
listTools(context: MCPContext) {
|
|
187
|
-
return this.registry.getAccessibleTools(context);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Get tool definition
|
|
192
|
-
*/
|
|
193
|
-
getTool(toolName: string, context: MCPContext) {
|
|
194
|
-
const server = this.registry.findServerByTool(toolName);
|
|
195
|
-
if (!server) return undefined;
|
|
196
|
-
|
|
197
|
-
if (!this.registry.canAccessTool(toolName, context)) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return server.tools.find((t) => t.name === toolName || `${server.id}:${t.name}` === toolName);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Global client instance
|
|
206
|
-
export const mcpClient = new MCPClient();
|
package/src/consent.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export interface ToolConsentGrant {
|
|
2
|
-
readonly tenantId: string;
|
|
3
|
-
readonly userId?: string;
|
|
4
|
-
readonly serverId: string;
|
|
5
|
-
readonly toolName: string;
|
|
6
|
-
readonly scopes: readonly string[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ToolConsentStore {
|
|
10
|
-
grant(grant: ToolConsentGrant): Promise<void>;
|
|
11
|
-
hasConsent(grant: ToolConsentGrant): Promise<boolean>;
|
|
12
|
-
list(tenantId?: string): Promise<ToolConsentGrant[]>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function consentKey(grant: Omit<ToolConsentGrant, "scopes">): string {
|
|
16
|
-
return [grant.tenantId, grant.userId ?? "*", grant.serverId, grant.toolName].join(":");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class InMemoryToolConsentStore implements ToolConsentStore {
|
|
20
|
-
readonly #grants = new Map<string, ToolConsentGrant>();
|
|
21
|
-
|
|
22
|
-
async grant(grant: ToolConsentGrant): Promise<void> {
|
|
23
|
-
this.#grants.set(consentKey(grant), { ...grant, scopes: [...grant.scopes] });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async hasConsent(grant: ToolConsentGrant): Promise<boolean> {
|
|
27
|
-
const exact = this.#grants.get(consentKey(grant));
|
|
28
|
-
const wildcard = this.#grants.get(
|
|
29
|
-
consentKey({
|
|
30
|
-
tenantId: grant.tenantId,
|
|
31
|
-
serverId: grant.serverId,
|
|
32
|
-
toolName: grant.toolName,
|
|
33
|
-
}),
|
|
34
|
-
);
|
|
35
|
-
const existing = exact ?? wildcard;
|
|
36
|
-
if (!existing) return false;
|
|
37
|
-
const scopes = new Set(existing.scopes);
|
|
38
|
-
return grant.scopes.every((scope) => scopes.has(scope));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async list(tenantId?: string): Promise<ToolConsentGrant[]> {
|
|
42
|
-
return Array.from(this.#grants.values()).filter(
|
|
43
|
-
(grant) => tenantId === undefined || grant.tenantId === tenantId,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/debug.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendCapabilityDebug,
|
|
3
|
-
capabilityDebugPath,
|
|
4
|
-
readCapabilityDebug,
|
|
5
|
-
} from "@nebutra/capability-kit/debug";
|
|
6
|
-
|
|
7
|
-
export interface ToolDebugEntry {
|
|
8
|
-
readonly at: string;
|
|
9
|
-
readonly type: "tool_call" | "connect" | "inspect";
|
|
10
|
-
readonly requestId?: string;
|
|
11
|
-
readonly tenantId?: string;
|
|
12
|
-
readonly serverId?: string;
|
|
13
|
-
readonly toolName?: string;
|
|
14
|
-
readonly ok: boolean;
|
|
15
|
-
readonly durationMs?: number;
|
|
16
|
-
readonly error?: string;
|
|
17
|
-
readonly suggestion?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function toolDebugPath(): string {
|
|
21
|
-
return capabilityDebugPath("tool-protocol");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function appendToolDebug(
|
|
25
|
-
entry: Omit<ToolDebugEntry, "at"> & { at?: string },
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
await appendCapabilityDebug("tool-protocol", entry);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function readToolDebug(limit = 10): Promise<ToolDebugEntry[]> {
|
|
31
|
-
return readCapabilityDebug("tool-protocol", { limit }) as Promise<ToolDebugEntry[]>;
|
|
32
|
-
}
|
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