1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
/**
* Tool name namespacing + ToolContract adapter.
*
* Namespaces each MCP tool as <serverId>__<toolName> (double underscore).
* Adapts an MCP tool definition into a Dispatch ToolContract whose execute()
* proxies to the MCP server via the injected McpToolCaller (client.callTool()).
*
* PURE: depends only on the McpToolCaller port — never on the concrete client.
*/
import type {
ToolContract,
ToolExecuteContext,
ToolParameterSchema,
ToolResult,
} from "@dispatch/kernel";
import type { McpContentItem, McpToolCaller, McpToolInfo } from "./types.js";
const NAMESPACE_SEP = "__";
export function namespace(serverId: string, toolName: string): string {
return `${serverId}${NAMESPACE_SEP}${toolName}`;
}
export function adaptTool(
serverId: string,
mcpTool: McpToolInfo,
caller: McpToolCaller,
): ToolContract {
const parameters: ToolParameterSchema = {
type: "object",
...(mcpTool.inputSchema.properties !== undefined && {
properties: mcpTool.inputSchema.properties,
}),
...(mcpTool.inputSchema.required !== undefined && {
required: mcpTool.inputSchema.required,
}),
...(mcpTool.inputSchema.additionalProperties !== undefined && {
additionalProperties: mcpTool.inputSchema.additionalProperties,
}),
};
return {
name: namespace(serverId, mcpTool.name),
description: `[${serverId}] ${mcpTool.description}`,
parameters,
concurrencySafe: false,
execute: async (args: unknown, ctx: ToolExecuteContext): Promise<ToolResult> => {
const result = await caller.callTool(mcpTool.name, args, ctx.signal);
const toolResult: ToolResult = {
content: flattenContent(result.content),
};
if (result.isError !== undefined) {
(toolResult as { isError?: boolean }).isError = result.isError;
}
return toolResult;
},
};
}
export function flattenContent(content: readonly McpContentItem[]): string {
if (content.length === 0) return "";
const parts: string[] = [];
for (const item of content) {
if (item.type === "text" && item.text !== undefined) {
parts.push(item.text);
} else if (item.type === "image" && item.mimeType !== undefined) {
parts.push(`[image: ${item.mimeType}]`);
} else if (item.type === "resource" && item.resource !== undefined) {
if (item.resource.text !== undefined) {
parts.push(item.resource.text);
} else {
parts.push(`[resource: ${item.resource.uri}]`);
}
}
}
return parts.join("\n");
}
|