import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
import type { ToolSchemaShape } from '../core/plugin-types.ts';
import { startDaemonBackground } from './daemon-control.ts';
import { DaemonClient } from './daemon-client.ts';
import { buildCliToolCatalogFromManifest, createToolCatalog } from '../runtime/tool-catalog.ts';
import type { ToolCatalog, ToolDefinition } from '../runtime/types.ts';
import { toKebabCase } from '../runtime/naming.ts';
import type { ToolResponse } from '../types/common.ts';
import { jsonSchemaToZod } from '../integrations/xcode-tools-bridge/jsonschema-to-zod.ts';
import { XcodeIdeToolService } from '../integrations/xcode-tools-bridge/tool-service.ts';
import { toLocalToolName } from '../integrations/xcode-tools-bridge/registry.ts';
import { log } from '../utils/logging/index.ts';
interface BuildCliToolCatalogOptions {
socketPath: string;
workspaceRoot: string;
cliExposedWorkflowIds: string[];
logLevel?: string;
discoveryMode?: 'none' | 'quick';
}
type JsonSchemaObject = {
properties?: Record<string, unknown>;
required?: unknown[];
};
function jsonSchemaToToolSchemaShape(inputSchema: unknown): ToolSchemaShape {
if (!inputSchema || typeof inputSchema !== 'object') {
return {};
}
const schema = inputSchema as JsonSchemaObject;
const properties = schema.properties;
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
return {};
}
const requiredFields = new Set(
Array.isArray(schema.required)
? schema.required.filter((name): name is string => typeof name === 'string')
: [],
);
const shape: ToolSchemaShape = {};
for (const [name, propertySchema] of Object.entries(properties)) {
const zodSchema = jsonSchemaToZod(propertySchema);
shape[name] = requiredFields.has(name) ? zodSchema : zodSchema.optional();
}
return shape;
}
function buildDaemonEnvOverrides(opts: BuildCliToolCatalogOptions): Record<string, string> {
const env: Record<string, string> = {};
if (opts.logLevel) {
env.XCODEBUILDMCP_DAEMON_LOG_LEVEL = opts.logLevel;
}
return env;
}
async function invokeRemoteToolOneShot(
remoteToolName: string,
args: Record<string, unknown>,
): Promise<ToolResponse> {
const service = new XcodeIdeToolService();
service.setWorkflowEnabled(true);
try {
const response = await service.invokeTool(remoteToolName, args);
return response as unknown as ToolResponse;
} finally {
await service.disconnect();
}
}
type DynamicBridgeTool = {
name: string;
description?: string;
inputSchema?: unknown;
annotations?: ToolAnnotations;
};
function createCliXcodeProxyTool(remoteTool: DynamicBridgeTool): ToolDefinition {
const cliSchema = jsonSchemaToToolSchemaShape(remoteTool.inputSchema);
return {
cliName: `xcode-ide-${toKebabCase(remoteTool.name)}`,
mcpName: toLocalToolName(remoteTool.name),
workflow: 'xcode-ide',
description: remoteTool.description ?? '',
annotations: remoteTool.annotations,
mcpSchema: cliSchema,
cliSchema,
stateful: false,
xcodeIdeRemoteToolName: remoteTool.name,
handler: async (params): Promise<ToolResponse> => {
return invokeRemoteToolOneShot(remoteTool.name, params);
},
};
}
async function loadDaemonBackedXcodeProxyTools(
opts: BuildCliToolCatalogOptions,
): Promise<ToolDefinition[]> {
const discoveryMode = opts.discoveryMode ?? 'none';
const quickMode = discoveryMode === 'quick';
const daemonClient = new DaemonClient({
socketPath: opts.socketPath,
timeout: quickMode ? 400 : 250,
});
try {
const isRunning = await daemonClient.isRunning();
if (!isRunning) {
if (!quickMode) {
return [];
}
// Fast path for CLI help/discovery: fire-and-forget daemon startup to avoid
// blocking command rendering while still warming a long-lived bridge session.
try {
startDaemonBackground({
socketPath: opts.socketPath,
workspaceRoot: opts.workspaceRoot,
env: buildDaemonEnvOverrides(opts),
});
} catch (startError) {
const message = startError instanceof Error ? startError.message : String(startError);
log('warning', `[xcode-ide] Failed to start daemon in background: ${message}`);
}
return [];
}
const tools = await daemonClient.listXcodeIdeTools({
refresh: false,
prefetch: quickMode,
});
return tools.map(
(tool): ToolDefinition =>
createCliXcodeProxyTool({
name: tool.remoteName,
description: tool.description,
inputSchema: tool.inputSchema,
annotations: tool.annotations,
}),
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (quickMode) {
log('warning', `[xcode-ide] CLI daemon-backed bridge discovery failed: ${message}`);
} else {
log('debug', `[xcode-ide] CLI cached bridge discovery skipped: ${message}`);
}
return [];
}
}
/**
* Build a tool catalog for CLI usage using the manifest system.
* CLI visibility is determined by manifest availability and predicates.
*/
export async function buildCliToolCatalog(opts: BuildCliToolCatalogOptions): Promise<ToolCatalog> {
const manifestCatalog = await buildCliToolCatalogFromManifest();
if (!opts.cliExposedWorkflowIds.includes('xcode-ide')) {
return manifestCatalog;
}
const dynamicTools = await loadDaemonBackedXcodeProxyTools(opts);
if (dynamicTools.length === 0) {
return manifestCatalog;
}
const existingCliNames = new Set(manifestCatalog.tools.map((tool) => tool.cliName));
const mergedTools = [
...manifestCatalog.tools,
...dynamicTools.filter((tool) => !existingCliNames.has(tool.cliName)),
];
return createToolCatalog(mergedTools);
}