summaryrefslogtreecommitdiffhomepage
path: root/packages/mcp/src/registry.ts
blob: 7c31c881df06f6e899c039ad1b00da6034b9f8db (plain)
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");
}