* `PluginToToolBridge` — converts the runtime view of MCP tools (advertised
* by an `McpRuntime`) into PilotDeck `ToolDefinition`s suitable for
* registration in `ToolRegistry`. Implements M10-M12 of §6.1:
*
* - M10 wire name `mcp__<serverId>__<toolName>` (already produced by
* `McpClient.listTools`).
* - M11 description ≤ 2048 chars (already truncated).
* - M12 annotations.readOnlyHint / destructiveHint / openWorldHint
* reflected onto the PilotDeck tool flags so the permission
* engine can decide whether to ask.
*
* Result transformation (M14): we currently emit a single `json` result
* block. The existing `ToolRuntime` already truncates oversized payloads
* via `maxResultBytes`; deferring the persisted-large-blob path for now
* (recorded as `intentional_difference` in the parity table).
*/
import { PilotDeckToolRuntimeError } from "../../tool/protocol/errors.js";
import type {
PilotDeckToolDefinition,
PilotDeckToolExecutionOutput,
PilotDeckToolInputSchema,
} from "../../tool/index.js";
import type { McpClient } from "../client/McpClient.js";
import type { McpRuntime } from "./McpRuntime.js";
import type {
PilotDeckMcpToolAnnotations,
PilotDeckMcpToolSpec,
} from "../protocol/types.js";
export type CreateToolDefinitionsOptions = {
callTimeoutMs?: number;
};
export async function createMcpToolDefinitionsFromRuntime(
runtime: McpRuntime,
options: CreateToolDefinitionsOptions = {},
): Promise<PilotDeckToolDefinition[]> {
const tools = await runtime.listAllTools();
return tools.map((spec) => buildToolDefinition(spec, runtime, options));
}
function buildToolDefinition(
spec: PilotDeckMcpToolSpec,
runtime: McpRuntime,
options: CreateToolDefinitionsOptions,
): PilotDeckToolDefinition {
const annotations: PilotDeckMcpToolAnnotations = spec.annotations ?? {};
const isReadOnly = annotations.readOnlyHint === true;
const isDestructive = annotations.destructiveHint === true;
const isOpenWorld = annotations.openWorldHint !== false;
const inputSchema = normalizeSchema(spec.inputSchema);
return {
name: spec.wireName,
description: spec.description,
kind: "mcp",
inputSchema,
maxResultBytes: 200_000,
isReadOnly: () => isReadOnly,
isConcurrencySafe: () => isReadOnly,
isDestructive: () => isDestructive,
isOpenWorld: () => isOpenWorld,
execute: async (input, context): Promise<PilotDeckToolExecutionOutput> => {
const client: McpClient | undefined = runtime.getClient(spec.serverId);
if (!client) {
throw new PilotDeckToolRuntimeError(
"unsupported_tool",
`MCP server ${spec.serverId} is not registered`,
);
}
try {
const { content, isError } = await client.callTool(spec.toolName, input, {
signal: context.abortSignal,
timeoutMs: options.callTimeoutMs,
});
if (isError === true) {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
extractMcpErrorText(content, spec.serverId, spec.toolName),
{ content },
);
}
return {
content: [{ type: "json", value: content }],
data: content,
metadata: {
mcp: { serverId: spec.serverId, toolName: spec.toolName, wireName: spec.wireName },
},
};
} catch (err) {
if (err instanceof PilotDeckToolRuntimeError) throw err;
const e = err as { code?: string; message?: string };
if (e.code === "mcp_call_timeout") {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
e.message ?? `MCP call timed out (${spec.serverId}/${spec.toolName})`,
{ errorCode: "mcp_call_timeout" },
);
}
if (e.code === "mcp_session_expired") {
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
e.message ?? `MCP session expired (${spec.serverId}/${spec.toolName})`,
{ errorCode: "mcp_session_expired" },
);
}
throw new PilotDeckToolRuntimeError(
"tool_execution_failed",
e.message ?? `MCP call failed (${spec.serverId}/${spec.toolName})`,
{ errorCode: e.code ?? "mcp_call_failed" },
);
}
},
};
}
function extractMcpErrorText(
content: unknown,
serverId: string,
toolName: string,
): string {
const fallback = `MCP server ${serverId}/${toolName} returned isError`;
if (!Array.isArray(content)) return fallback;
const texts = content
.filter(
(block: unknown): block is { type: string; text: string } =>
!!block &&
typeof block === "object" &&
(block as { type?: string }).type === "text" &&
typeof (block as { text?: string }).text === "string",
)
.map((block) => block.text);
if (texts.length === 0) return fallback;
return texts.join("\n");
}
function normalizeSchema(raw: unknown): PilotDeckToolInputSchema {
if (raw && typeof raw === "object") {
const obj = raw as PilotDeckToolInputSchema;
if (obj.type === "object") return obj;
}
return { type: "object", additionalProperties: true, properties: {} };
}