dexto 1.13.4 → 1.14.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/dist/api/mcp/http-gateway.d.ts +21 -0
- package/dist/api/mcp/http-gateway.d.ts.map +1 -0
- package/dist/api/mcp/http-gateway.js +110 -0
- package/dist/api/mcp/tool-aggregation-handler.d.ts +9 -6
- package/dist/api/mcp/tool-aggregation-handler.d.ts.map +1 -1
- package/dist/api/mcp/tool-aggregation-handler.js +203 -70
- package/dist/api/server-hono.d.ts +2 -0
- package/dist/api/server-hono.d.ts.map +1 -1
- package/dist/api/server-hono.js +377 -276
- package/dist/cli/approval/headless-approval-handler.d.ts +4 -0
- package/dist/cli/approval/headless-approval-handler.d.ts.map +1 -0
- package/dist/cli/approval/headless-approval-handler.js +19 -0
- package/dist/cli/auth/api-client.js +1 -1
- package/dist/cli/auth/constants.d.ts +1 -0
- package/dist/cli/auth/constants.d.ts.map +1 -1
- package/dist/cli/auth/constants.js +2 -1
- package/dist/cli/auth/dexto-api-key.d.ts.map +1 -1
- package/dist/cli/auth/dexto-api-key.js +3 -0
- package/dist/cli/auth/login-persistence.d.ts +1 -1
- package/dist/cli/auth/login-persistence.d.ts.map +1 -1
- package/dist/cli/auth/login-persistence.js +9 -1
- package/dist/cli/auth/service.d.ts +2 -0
- package/dist/cli/auth/service.d.ts.map +1 -1
- package/dist/cli/auth/service.js +1 -0
- package/dist/cli/commands/agents/register.d.ts.map +1 -1
- package/dist/cli/commands/agents/register.js +0 -1
- package/dist/cli/commands/auth/login.d.ts.map +1 -1
- package/dist/cli/commands/auth/login.js +9 -4
- package/dist/cli/commands/cloud/discovery.d.ts +17 -0
- package/dist/cli/commands/cloud/discovery.d.ts.map +1 -0
- package/dist/cli/commands/cloud/discovery.js +88 -0
- package/dist/cli/commands/cloud/register.d.ts +3 -0
- package/dist/cli/commands/cloud/register.d.ts.map +1 -0
- package/dist/cli/commands/cloud/register.js +27 -0
- package/dist/cli/commands/mcp/register.d.ts +1 -1
- package/dist/cli/commands/mcp/register.d.ts.map +1 -1
- package/dist/cli/commands/mcp/register.js +259 -26
- package/dist/cli/commands/mcp/standalone.d.ts +98 -0
- package/dist/cli/commands/mcp/standalone.d.ts.map +1 -0
- package/dist/cli/commands/mcp/standalone.js +489 -0
- package/dist/cli/commands/run/headless.d.ts +3 -1
- package/dist/cli/commands/run/headless.d.ts.map +1 -1
- package/dist/cli/commands/run/headless.js +13 -1
- package/dist/cli/commands/run/output.d.ts +21 -0
- package/dist/cli/commands/run/output.d.ts.map +1 -0
- package/dist/cli/commands/run/output.js +95 -0
- package/dist/cli/commands/run/register.d.ts.map +1 -1
- package/dist/cli/commands/run/register.js +27 -3
- package/dist/cli/commands/setup.d.ts +2 -2
- package/dist/cli/commands/trace/client.d.ts +2 -2
- package/dist/cli/utils/options.js +3 -1
- package/dist/config/cli-overrides.d.ts +3 -1
- package/dist/config/cli-overrides.d.ts.map +1 -1
- package/dist/config/cli-overrides.js +9 -3
- package/dist/index-main.js +11 -10
- package/dist/utils/env.d.ts +2 -0
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +6 -0
- package/dist/utils/graceful-shutdown.d.ts +1 -1
- package/dist/utils/graceful-shutdown.d.ts.map +1 -1
- package/dist/utils/graceful-shutdown.js +28 -9
- package/package.json +14 -14
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import type { ValidatedServersConfig } from '@dexto/core/mcp';
|
|
3
|
+
import type { HttpAuthorization } from '@dexto/server/hono/auth';
|
|
4
|
+
export interface McpHttpAggregationGateway {
|
|
5
|
+
port: number;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
interface HttpAggregationOptions {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
strict: boolean;
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
authorize: HttpAuthorization;
|
|
15
|
+
}
|
|
16
|
+
/** HTTP authority checks protect open loopback listeners against browser rebinding. */
|
|
17
|
+
export declare function allowsMcpHttpRequestOrigin(host: string, port: number, headers: IncomingHttpHeaders): boolean;
|
|
18
|
+
/** Stateless MCP HTTP requests share one host-owned upstream discovery snapshot. */
|
|
19
|
+
export declare function startMcpHttpAggregationGateway(configs: ValidatedServersConfig, options: HttpAggregationOptions): Promise<McpHttpAggregationGateway>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=http-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-gateway.d.ts","sourceRoot":"","sources":["../../../src/api/mcp/http-gateway.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAGjE,MAAM,WAAW,yBAAyB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,UAAU,sBAAsB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,iBAAiB,CAAC;CAChC;AAED,uFAAuF;AACvF,wBAAgB,0BAA0B,CACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,mBAAmB,GAC7B,OAAO,CAQT;AAED,oFAAoF;AACpF,wBAAsB,8BAA8B,CAChD,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,sBAAsB,GAChC,OAAO,CAAC,yBAAyB,CAAC,CAqGpC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { initializeMcpToolAggregationHost } from './tool-aggregation-handler.js';
|
|
5
|
+
/** HTTP authority checks protect open loopback listeners against browser rebinding. */
|
|
6
|
+
export function allowsMcpHttpRequestOrigin(host, port, headers) {
|
|
7
|
+
const listenerHost = isIP(host) === 6 ? `[${host}]` : host;
|
|
8
|
+
const listener = new URL(`http://${listenerHost}:${port}`);
|
|
9
|
+
const loopback = host === '127.0.0.1' || host === '::1';
|
|
10
|
+
return ((!loopback || headers.host === listener.host) &&
|
|
11
|
+
(headers.origin === undefined || headers.origin === listener.origin));
|
|
12
|
+
}
|
|
13
|
+
/** Stateless MCP HTTP requests share one host-owned upstream discovery snapshot. */
|
|
14
|
+
export async function startMcpHttpAggregationGateway(configs, options) {
|
|
15
|
+
const owner = await initializeMcpToolAggregationHost(configs, options.name, options.version, options.strict);
|
|
16
|
+
const server = createServer((request, response) => {
|
|
17
|
+
// Authorization precedes routing, body parsing and downstream protocol allocation.
|
|
18
|
+
const authorization = options.authorize(request.headers.authorization);
|
|
19
|
+
if (!authorization.allowed) {
|
|
20
|
+
response.writeHead(authorization.status, { 'content-type': 'application/json' });
|
|
21
|
+
response.end(JSON.stringify(authorization.body));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const address = server.address();
|
|
25
|
+
if (address === null || typeof address === 'string') {
|
|
26
|
+
response.writeHead(503).end();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!allowsMcpHttpRequestOrigin(options.host, address.port, request.headers)) {
|
|
30
|
+
response.writeHead(403).end();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (request.url?.split('?')[0] !== '/mcp') {
|
|
34
|
+
response.writeHead(404).end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (request.method !== 'POST') {
|
|
38
|
+
response.writeHead(405, { allow: 'POST' }).end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// SDK stateless transports cannot be reused, even by a single HTTP client.
|
|
42
|
+
const transport = new StreamableHTTPServerTransport({
|
|
43
|
+
enableJsonResponse: true,
|
|
44
|
+
});
|
|
45
|
+
// The SDK class implements Transport, but its callback getters include undefined,
|
|
46
|
+
// conflicting with Transport's optional properties under exactOptionalPropertyTypes.
|
|
47
|
+
const peer = owner.connect(transport);
|
|
48
|
+
let closePromise;
|
|
49
|
+
const closeRequest = () => {
|
|
50
|
+
response.off('close', closeRequest);
|
|
51
|
+
response.off('finish', closeRequest);
|
|
52
|
+
if (closePromise !== undefined)
|
|
53
|
+
return;
|
|
54
|
+
closePromise = peer.then((connection) => connection.close());
|
|
55
|
+
void closePromise.catch(() => {
|
|
56
|
+
process.stderr.write('MCP HTTP request cleanup failed.\n');
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
response.once('close', closeRequest);
|
|
60
|
+
response.once('finish', closeRequest);
|
|
61
|
+
void peer
|
|
62
|
+
.then(() => transport.handleRequest(request, response))
|
|
63
|
+
.catch(() => {
|
|
64
|
+
if (!response.headersSent)
|
|
65
|
+
response.writeHead(500);
|
|
66
|
+
response.end();
|
|
67
|
+
closeRequest();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
let closePromise;
|
|
71
|
+
const close = () => {
|
|
72
|
+
closePromise ??= Promise.resolve().then(async () => {
|
|
73
|
+
const listenerClosed = server.listening
|
|
74
|
+
? new Promise((resolve, reject) => {
|
|
75
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
76
|
+
server.closeAllConnections();
|
|
77
|
+
})
|
|
78
|
+
: Promise.resolve();
|
|
79
|
+
const results = await Promise.allSettled([listenerClosed, owner.close()]);
|
|
80
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
81
|
+
if (failures.length > 0) {
|
|
82
|
+
throw new AggregateError(failures.map((result) => result.reason), 'MCP HTTP gateway cleanup failed');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return closePromise;
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
const onError = (error) => {
|
|
90
|
+
server.off('listening', onListening);
|
|
91
|
+
reject(error);
|
|
92
|
+
};
|
|
93
|
+
const onListening = () => {
|
|
94
|
+
server.off('error', onError);
|
|
95
|
+
resolve();
|
|
96
|
+
};
|
|
97
|
+
server.once('error', onError);
|
|
98
|
+
server.once('listening', onListening);
|
|
99
|
+
server.listen(options.port, options.host);
|
|
100
|
+
});
|
|
101
|
+
const address = server.address();
|
|
102
|
+
if (address === null || typeof address === 'string')
|
|
103
|
+
throw new Error('MCP HTTP listener address unavailable');
|
|
104
|
+
return { port: address.port, close };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await close().catch(() => undefined);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
3
|
-
import { type ValidatedServersConfig } from '@dexto/core';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { type ValidatedServersConfig } from '@dexto/core/mcp';
|
|
4
|
+
export interface McpToolAggregationHost {
|
|
5
|
+
connect(transport: Transport): Promise<McpServer>;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** One upstream discovery snapshot shared by independently connected downstream peers. */
|
|
9
|
+
export declare function initializeMcpToolAggregationHost(serverConfigs: ValidatedServersConfig, serverName: string, serverVersion: string, strict: boolean): Promise<McpToolAggregationHost>;
|
|
10
|
+
/** Stdio-compatible owner: closing its downstream peer also closes its upstream snapshot. */
|
|
11
|
+
export declare function initializeMcpToolAggregationServer(serverConfigs: ValidatedServersConfig, mcpTransport: Transport, serverName: string, serverVersion: string, strict: boolean): Promise<McpServer>;
|
|
9
12
|
//# sourceMappingURL=tool-aggregation-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-aggregation-handler.d.ts","sourceRoot":"","sources":["../../../src/api/mcp/tool-aggregation-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,
|
|
1
|
+
{"version":3,"file":"tool-aggregation-handler.d.ts","sourceRoot":"","sources":["../../../src/api/mcp/tool-aggregation-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,EAA4B,KAAK,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAiBxF,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,0FAA0F;AAC1F,wBAAsB,gCAAgC,CAClD,aAAa,EAAE,sBAAsB,EACrC,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,OAAO,GAChB,OAAO,CAAC,sBAAsB,CAAC,CA8PjC;AAED,6FAA6F;AAC7F,wBAAsB,kCAAkC,CACpD,aAAa,EAAE,sBAAsB,EACrC,YAAY,EAAE,SAAS,EACvB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,OAAO,GAChB,OAAO,CAAC,SAAS,CAAC,CAuBpB"}
|
|
@@ -1,92 +1,225 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { MCPManager,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export async function initializeMcpToolAggregationServer(serverConfigs, mcpTransport, serverName, serverVersion, _strict) {
|
|
9
|
-
// Create MCP manager with no confirmation provider (tools are auto-approved)
|
|
2
|
+
import { MCPManager, MCPErrorCode } from '@dexto/core/mcp';
|
|
3
|
+
import { createLogger, DextoLogComponent } from '@dexto/core/logger';
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, McpError, ErrorCode, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
/** One upstream discovery snapshot shared by independently connected downstream peers. */
|
|
6
|
+
export async function initializeMcpToolAggregationHost(serverConfigs, serverName, serverVersion, strict) {
|
|
7
|
+
// Manager diagnostics can contain upstream payloads; keep them off protocol streams.
|
|
10
8
|
const mcpLogger = createLogger({
|
|
11
9
|
config: {
|
|
12
10
|
level: 'info',
|
|
13
|
-
transports: [{ type: '
|
|
11
|
+
transports: [{ type: 'silent' }],
|
|
14
12
|
},
|
|
15
13
|
agentId: 'mcp-tool-aggregation',
|
|
16
14
|
component: DextoLogComponent.MCP,
|
|
17
15
|
});
|
|
18
16
|
const mcpManager = new MCPManager(mcpLogger);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let toolCount = 0;
|
|
32
|
-
for (const [toolName, toolDef] of Object.entries(toolDefinitions)) {
|
|
33
|
-
toolCount++;
|
|
34
|
-
const jsonSchema = toolDef.parameters ?? { type: 'object', properties: {} };
|
|
35
|
-
const paramsShape = jsonSchemaToZodShape(jsonSchema);
|
|
36
|
-
const _paramsSchema = z.object(paramsShape);
|
|
37
|
-
logger.debug(`Registering tool '${toolName}' with schema: ${JSON.stringify(jsonSchema)}`);
|
|
38
|
-
mcpServer.tool(toolName, toolDef.description || `Tool: ${toolName}`, paramsShape, async (args) => {
|
|
39
|
-
logger.info(`Tool aggregation: executing ${toolName}`);
|
|
40
|
-
try {
|
|
41
|
-
const result = await mcpManager.executeTool(toolName, args);
|
|
42
|
-
logger.info(`Tool aggregation: ${toolName} completed successfully`);
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
logger.error(`Tool aggregation: ${toolName} failed: ${error}`);
|
|
47
|
-
throw error;
|
|
17
|
+
const peers = new Map();
|
|
18
|
+
const attaching = new Set();
|
|
19
|
+
let closePromise;
|
|
20
|
+
const close = () => {
|
|
21
|
+
closePromise ??= Promise.resolve().then(async () => {
|
|
22
|
+
await Promise.allSettled(Array.from(attaching));
|
|
23
|
+
const results = await Promise.allSettled(Array.from(peers.values(), (closePeer) => closePeer()));
|
|
24
|
+
results.push(...(await Promise.allSettled([mcpManager.disconnectAll()])));
|
|
25
|
+
results.push(...(await Promise.allSettled([mcpLogger.destroy()])));
|
|
26
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
27
|
+
if (failures.length > 0) {
|
|
28
|
+
throw new AggregateError(failures.map((result) => result.reason), 'MCP aggregation cleanup failed');
|
|
48
29
|
}
|
|
49
30
|
});
|
|
31
|
+
return closePromise;
|
|
32
|
+
};
|
|
33
|
+
const connectionConfigs = { ...serverConfigs };
|
|
34
|
+
if (strict) {
|
|
35
|
+
for (const [name, config] of Object.entries(serverConfigs)) {
|
|
36
|
+
if (config.enabled)
|
|
37
|
+
connectionConfigs[name] = { ...config, connectionMode: 'strict' };
|
|
38
|
+
}
|
|
50
39
|
}
|
|
51
|
-
logger.info(`Registered ${toolCount} tools from connected MCP servers`);
|
|
52
|
-
// Register resources if available
|
|
53
40
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
await mcpManager.initializeFromConfig(connectionConfigs);
|
|
42
|
+
if (Object.values(mcpManager.getFailedConnections()).some((error) => error.code === MCPErrorCode.DUPLICATE_NAME)) {
|
|
43
|
+
throw new Error('Duplicate upstream server identity');
|
|
44
|
+
}
|
|
45
|
+
const tools = new Map();
|
|
46
|
+
const discovered = new Map();
|
|
47
|
+
await Promise.all(Array.from(mcpManager.getClients(), async ([name, connection]) => {
|
|
48
|
+
const config = serverConfigs[name];
|
|
49
|
+
if (config === undefined)
|
|
50
|
+
throw new Error('Missing upstream connection configuration');
|
|
51
|
+
const client = await connection.getConnectedClient();
|
|
52
|
+
const capabilities = client.getServerCapabilities();
|
|
53
|
+
const options = { timeout: config.timeout };
|
|
54
|
+
const [toolResult, resourceResult, promptResult] = await Promise.all([
|
|
55
|
+
capabilities?.tools ? client.listTools(undefined, options) : { tools: [] },
|
|
56
|
+
capabilities?.resources
|
|
57
|
+
? client.listResources(undefined, options)
|
|
58
|
+
: { resources: [] },
|
|
59
|
+
capabilities?.prompts
|
|
60
|
+
? client.listPrompts(undefined, options)
|
|
61
|
+
: { prompts: [] },
|
|
62
|
+
]);
|
|
63
|
+
const upstreamTools = new Map(toolResult.tools.map((tool) => [tool.name, tool]));
|
|
64
|
+
if (upstreamTools.size !== toolResult.tools.length)
|
|
65
|
+
throw new Error('Duplicate upstream tool identity');
|
|
66
|
+
const upstreamResources = new Map(resourceResult.resources.map((resource) => [resource.uri, resource]));
|
|
67
|
+
if (upstreamResources.size !== resourceResult.resources.length)
|
|
68
|
+
throw new Error('Duplicate upstream resource identity');
|
|
69
|
+
discovered.set(name, {
|
|
70
|
+
client,
|
|
71
|
+
tools: upstreamTools,
|
|
72
|
+
resources: upstreamResources,
|
|
73
|
+
prompts: promptResult.prompts,
|
|
74
|
+
timeout: config.timeout,
|
|
64
75
|
});
|
|
65
|
-
});
|
|
76
|
+
}));
|
|
77
|
+
let discoveredCount = 0;
|
|
78
|
+
for (const upstream of discovered.values())
|
|
79
|
+
discoveredCount += upstream.tools.size;
|
|
80
|
+
const aliases = mcpManager.getAllToolsWithServerInfo();
|
|
81
|
+
if (aliases.size !== discoveredCount)
|
|
82
|
+
throw new Error('Ambiguous aggregation tool aliases');
|
|
83
|
+
for (const [alias, entry] of aliases) {
|
|
84
|
+
const upstream = discovered.get(entry.serverName);
|
|
85
|
+
const definition = upstream?.tools.get(entry.upstreamToolName);
|
|
86
|
+
if (upstream === undefined || definition === undefined) {
|
|
87
|
+
throw new Error('Aggregation tool identity changed during startup');
|
|
88
|
+
}
|
|
89
|
+
tools.set(alias, {
|
|
90
|
+
definition: { ...definition, name: alias },
|
|
91
|
+
client: upstream.client,
|
|
92
|
+
name: entry.upstreamToolName,
|
|
93
|
+
timeout: upstream.timeout,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const resources = new Map();
|
|
97
|
+
const cachedResources = await mcpManager.listAllResources();
|
|
98
|
+
let discoveredResourceCount = 0;
|
|
99
|
+
for (const upstream of discovered.values())
|
|
100
|
+
discoveredResourceCount += upstream.resources.size;
|
|
101
|
+
if (cachedResources.length !== discoveredResourceCount)
|
|
102
|
+
throw new Error('Ambiguous aggregation resource identities');
|
|
103
|
+
for (const resource of cachedResources) {
|
|
104
|
+
const upstream = discovered.get(resource.serverName);
|
|
105
|
+
const definition = upstream?.resources.get(resource.summary.uri);
|
|
106
|
+
if (upstream === undefined || definition === undefined)
|
|
107
|
+
throw new Error('Aggregation resource identity changed during startup');
|
|
108
|
+
resources.set(resource.key, {
|
|
109
|
+
definition: { ...definition, uri: resource.key },
|
|
110
|
+
client: upstream.client,
|
|
111
|
+
uri: definition.uri,
|
|
112
|
+
timeout: upstream.timeout,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const prompts = new Map();
|
|
116
|
+
for (const upstream of discovered.values()) {
|
|
117
|
+
for (const prompt of upstream.prompts) {
|
|
118
|
+
if (prompts.has(prompt.name))
|
|
119
|
+
throw new Error('Duplicate aggregation prompt identity');
|
|
120
|
+
prompts.set(prompt.name, {
|
|
121
|
+
definition: prompt,
|
|
122
|
+
client: upstream.client,
|
|
123
|
+
timeout: upstream.timeout,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
close,
|
|
129
|
+
async connect(transport) {
|
|
130
|
+
if (closePromise !== undefined)
|
|
131
|
+
throw new Error('MCP aggregation host is closing');
|
|
132
|
+
const attachment = (async () => {
|
|
133
|
+
const mcpServer = new McpServer({ name: serverName, version: serverVersion }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
134
|
+
const originalClose = mcpServer.close.bind(mcpServer);
|
|
135
|
+
let peerClosePromise;
|
|
136
|
+
const closePeer = () => (peerClosePromise ??= originalClose());
|
|
137
|
+
mcpServer.close = closePeer;
|
|
138
|
+
peers.set(mcpServer, closePeer);
|
|
139
|
+
const previousClose = mcpServer.server.onclose;
|
|
140
|
+
mcpServer.server.onclose = () => {
|
|
141
|
+
peers.delete(mcpServer);
|
|
142
|
+
previousClose?.();
|
|
143
|
+
};
|
|
144
|
+
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
145
|
+
tools: Array.from(tools.values(), (tool) => tool.definition),
|
|
146
|
+
}));
|
|
147
|
+
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
148
|
+
const tool = tools.get(request.params.name);
|
|
149
|
+
if (tool === undefined)
|
|
150
|
+
throw new McpError(ErrorCode.InvalidParams, 'Unknown aggregation tool');
|
|
151
|
+
return tool.client.callTool({ ...request.params, name: tool.name }, undefined, {
|
|
152
|
+
signal: extra.signal,
|
|
153
|
+
timeout: tool.timeout,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
mcpServer.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
157
|
+
resources: Array.from(resources.values(), (resource) => resource.definition),
|
|
158
|
+
}));
|
|
159
|
+
mcpServer.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
|
|
160
|
+
const resource = resources.get(request.params.uri);
|
|
161
|
+
if (resource === undefined)
|
|
162
|
+
throw new McpError(ErrorCode.InvalidParams, 'Unknown aggregation resource');
|
|
163
|
+
return resource.client.readResource({ ...request.params, uri: resource.uri }, { signal: extra.signal, timeout: resource.timeout });
|
|
164
|
+
});
|
|
165
|
+
mcpServer.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
166
|
+
prompts: Array.from(prompts.values(), (prompt) => prompt.definition),
|
|
167
|
+
}));
|
|
168
|
+
mcpServer.server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => {
|
|
169
|
+
const prompt = prompts.get(request.params.name);
|
|
170
|
+
if (prompt === undefined)
|
|
171
|
+
throw new McpError(ErrorCode.InvalidParams, 'Unknown aggregation prompt');
|
|
172
|
+
return prompt.client.getPrompt(request.params, {
|
|
173
|
+
signal: extra.signal,
|
|
174
|
+
timeout: prompt.timeout,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
await mcpServer.connect(transport);
|
|
179
|
+
if (closePromise !== undefined)
|
|
180
|
+
throw new Error('MCP aggregation host is closing');
|
|
181
|
+
return mcpServer;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
await closePeer().catch(() => undefined);
|
|
185
|
+
peers.delete(mcpServer);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
attaching.add(attachment);
|
|
190
|
+
try {
|
|
191
|
+
return await attachment;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
attaching.delete(attachment);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
66
198
|
}
|
|
67
199
|
catch (error) {
|
|
68
|
-
|
|
200
|
+
await close().catch(() => undefined);
|
|
201
|
+
throw error;
|
|
69
202
|
}
|
|
70
|
-
|
|
203
|
+
}
|
|
204
|
+
/** Stdio-compatible owner: closing its downstream peer also closes its upstream snapshot. */
|
|
205
|
+
export async function initializeMcpToolAggregationServer(serverConfigs, mcpTransport, serverName, serverVersion, strict) {
|
|
206
|
+
const host = await initializeMcpToolAggregationHost(serverConfigs, serverName, serverVersion, strict);
|
|
71
207
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}
|
|
208
|
+
const server = await host.connect(mcpTransport);
|
|
209
|
+
server.close = host.close;
|
|
210
|
+
const previousClose = server.server.onclose;
|
|
211
|
+
server.server.onclose = () => {
|
|
212
|
+
try {
|
|
213
|
+
previousClose?.();
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
void host.close().catch(() => undefined);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return server;
|
|
83
220
|
}
|
|
84
221
|
catch (error) {
|
|
85
|
-
|
|
222
|
+
await host.close().catch(() => undefined);
|
|
223
|
+
throw error;
|
|
86
224
|
}
|
|
87
|
-
// Connect server to transport
|
|
88
|
-
logger.info(`Connecting MCP tool aggregation server...`);
|
|
89
|
-
await mcpServer.connect(mcpTransport);
|
|
90
|
-
logger.info(`✅ MCP tool aggregation server connected with ${toolCount} tools exposed`);
|
|
91
|
-
return mcpServer;
|
|
92
225
|
}
|
|
@@ -3,6 +3,7 @@ import { DextoAgent } from '@dexto/core';
|
|
|
3
3
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
4
4
|
import { createDextoApp, createNodeServer, type WebUIRuntimeConfig } from '@dexto/server';
|
|
5
5
|
export type HonoInitializationResult = {
|
|
6
|
+
stop: () => Promise<void>;
|
|
6
7
|
app: ReturnType<typeof createDextoApp>;
|
|
7
8
|
server: ReturnType<typeof createNodeServer>['server'];
|
|
8
9
|
webhookSubscriber?: NonNullable<ReturnType<typeof createNodeServer>['webhookSubscriber']>;
|
|
@@ -26,6 +27,7 @@ export type HonoInitializationResult = {
|
|
|
26
27
|
export declare function initializeHonoApi(agent: DextoAgent, agentCardOverride?: Partial<AgentCard>, listenPort?: number, agentId?: string, configFilePath?: string, workspaceRoot?: string, webRoot?: string, webUIConfig?: WebUIRuntimeConfig): Promise<HonoInitializationResult>;
|
|
27
28
|
export declare function startHonoApiServer(agent: DextoAgent, port?: number, agentCardOverride?: Partial<AgentCard>, agentId?: string, configFilePath?: string, workspaceRoot?: string, webRoot?: string, webUIConfig?: WebUIRuntimeConfig): Promise<{
|
|
28
29
|
server: ReturnType<typeof createNodeServer>['server'];
|
|
30
|
+
stop: () => Promise<void>;
|
|
29
31
|
webhookSubscriber?: NonNullable<ReturnType<typeof createNodeServer>['webhookSubscriber']>;
|
|
30
32
|
}>;
|
|
31
33
|
//# sourceMappingURL=server-hono.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-hono.d.ts","sourceRoot":"","sources":["../../src/api/server-hono.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAuC,MAAM,aAAa,CAAC;AAY9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,EACH,cAAc,EACd,gBAAgB,EAWhB,KAAK,kBAAkB,EAC1B,MAAM,eAAe,CAAC;AAmGvB,MAAM,MAAM,wBAAwB,GAAG;IACnC,GAAG,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IACvC,MAAM,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC;IACtD,iBAAiB,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC;IAC1F,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,oBAAoB,EAAE,MAAM,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CAC9C,CAAC;AAGF,wBAAsB,iBAAiB,CACnC,KAAK,EAAE,UAAU,EACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EACtC,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,MAAM,EAChB,cAAc,CAAC,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,kBAAkB,GACjC,OAAO,CAAC,wBAAwB,CAAC,
|
|
1
|
+
{"version":3,"file":"server-hono.d.ts","sourceRoot":"","sources":["../../src/api/server-hono.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAuC,MAAM,aAAa,CAAC;AAY9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,EACH,cAAc,EACd,gBAAgB,EAWhB,KAAK,kBAAkB,EAC1B,MAAM,eAAe,CAAC;AAmGvB,MAAM,MAAM,wBAAwB,GAAG;IACnC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,GAAG,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IACvC,MAAM,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC;IACtD,iBAAiB,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC;IAC1F,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,oBAAoB,EAAE,MAAM,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CAC9C,CAAC;AAGF,wBAAsB,iBAAiB,CACnC,KAAK,EAAE,UAAU,EACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EACtC,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,MAAM,EAChB,cAAc,CAAC,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,kBAAkB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAsdnC;AAED,wBAAsB,kBAAkB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,SAAO,EACX,iBAAiB,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,EACtC,OAAO,CAAC,EAAE,MAAM,EAChB,cAAc,CAAC,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,kBAAkB,GACjC,OAAO,CAAC;IACP,MAAM,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC;IACtD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,iBAAiB,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC;CAC7F,CAAC,CAkDD"}
|