From 3793d8bc06ae472b12ce7087f2d36d00423168f4 Mon Sep 17 00:00:00 2001 From: MQ37 Date: Thu, 9 Oct 2025 16:13:18 -0700 Subject: [PATCH 1/2] fix: clone also the internal tools for skyfire mode --- src/mcp/server.ts | 29 +++++++++++++++++++++-------- src/utils/tools.ts | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 9af2cec2..b7f9cd5b 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -39,6 +39,7 @@ import { callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } f import { decodeDotPropertyNames } from '../tools/utils.js'; import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js'; import { buildActorResponseContent } from '../utils/actor-response.js'; +import { cloneToolEntry } from '../utils/tools.js'; import { buildMCPResponse } from '../utils/mcp.js'; import { createProgressTracker } from '../utils/progress.js'; import { getToolPublicFieldOnly } from '../utils/tools.js'; @@ -262,22 +263,23 @@ export class ActorsMcpServer { * @returns Array of added/updated tool wrappers */ public upsertTools(tools: ToolEntry[], shouldNotifyToolsChangedHandler = false) { - for (const wrap of tools) { - this.tools.set(wrap.tool.name, wrap); - } - // Handle Skyfire mode modifications once per tool upsert + // Handle Skyfire mode modifications before storing tools if (this.options.skyfireMode) { for (const wrap of tools) { if (wrap.type === 'actor' || (wrap.type === 'internal' && wrap.tool.name === HelperTools.ACTOR_CALL) || (wrap.type === 'internal' && wrap.tool.name === HelperTools.ACTOR_OUTPUT_GET)) { + + // Clone the tool before modifying it to avoid affecting shared objects + const clonedWrap = cloneToolEntry(wrap); + // Add Skyfire instructions to description if not already present - if (!wrap.tool.description.includes(SKYFIRE_TOOL_INSTRUCTIONS)) { - wrap.tool.description += `\n\n${SKYFIRE_TOOL_INSTRUCTIONS}`; + if (!clonedWrap.tool.description.includes(SKYFIRE_TOOL_INSTRUCTIONS)) { + clonedWrap.tool.description += `\n\n${SKYFIRE_TOOL_INSTRUCTIONS}`; } // Add skyfire-pay-id property if not present - if (wrap.tool.inputSchema && 'properties' in wrap.tool.inputSchema) { - const props = wrap.tool.inputSchema.properties as Record; + if (clonedWrap.tool.inputSchema && 'properties' in clonedWrap.tool.inputSchema) { + const props = clonedWrap.tool.inputSchema.properties as Record; if (!props['skyfire-pay-id']) { props['skyfire-pay-id'] = { type: 'string', @@ -285,8 +287,19 @@ export class ActorsMcpServer { }; } } + + // Store the cloned and modified tool + this.tools.set(clonedWrap.tool.name, clonedWrap); + } else { + // Store unmodified tools as-is + this.tools.set(wrap.tool.name, wrap); } } + } else { + // No skyfire mode - store tools as-is + for (const wrap of tools) { + this.tools.set(wrap.tool.name, wrap); + } } if (shouldNotifyToolsChangedHandler) this.notifyToolsChangedHandler(); return tools; diff --git a/src/utils/tools.ts b/src/utils/tools.ts index 7d8f0dbf..2772adbc 100644 --- a/src/utils/tools.ts +++ b/src/utils/tools.ts @@ -27,3 +27,27 @@ export function getExpectedToolsByCategories(categories: ToolCategory[]): ToolEn export function getExpectedToolNamesByCategories(categories: ToolCategory[]): string[] { return getExpectedToolsByCategories(categories).map((tool) => tool.tool.name); } + +/** + * Creates a deep copy of a tool entry, preserving functions like ajvValidate and call + * while cloning all other properties to avoid shared state mutations. + */ +export function cloneToolEntry(toolEntry: ToolEntry): ToolEntry { + // Store the original functions + const originalAjvValidate = toolEntry.tool.ajvValidate; + const originalCall = (toolEntry.tool as any).call; + + // Create a deep copy using JSON serialization (excluding functions) + const cloned = JSON.parse(JSON.stringify(toolEntry, (key, value) => { + if (key === 'ajvValidate' || key === 'call') return undefined; + return value; + })) as ToolEntry; + + // Restore the original functions + cloned.tool.ajvValidate = originalAjvValidate; + if (toolEntry.type === 'internal' && originalCall) { + (cloned.tool as any).call = originalCall; + } + + return cloned; +} From d6be45b8848f3d6f17c80f5d768acb728ed25ed7 Mon Sep 17 00:00:00 2001 From: MQ37 Date: Thu, 9 Oct 2025 16:16:14 -0700 Subject: [PATCH 2/2] fix lint --- src/mcp/server.ts | 8 +++----- src/utils/tools.ts | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index b7f9cd5b..6b966d72 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -39,10 +39,9 @@ import { callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } f import { decodeDotPropertyNames } from '../tools/utils.js'; import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js'; import { buildActorResponseContent } from '../utils/actor-response.js'; -import { cloneToolEntry } from '../utils/tools.js'; import { buildMCPResponse } from '../utils/mcp.js'; import { createProgressTracker } from '../utils/progress.js'; -import { getToolPublicFieldOnly } from '../utils/tools.js'; +import { cloneToolEntry, getToolPublicFieldOnly } from '../utils/tools.js'; import { connectMCPClient } from './client.js'; import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC, LOG_LEVEL_MAP } from './const.js'; import { processParamsGetTools } from './utils.js'; @@ -269,10 +268,9 @@ export class ActorsMcpServer { if (wrap.type === 'actor' || (wrap.type === 'internal' && wrap.tool.name === HelperTools.ACTOR_CALL) || (wrap.type === 'internal' && wrap.tool.name === HelperTools.ACTOR_OUTPUT_GET)) { - // Clone the tool before modifying it to avoid affecting shared objects const clonedWrap = cloneToolEntry(wrap); - + // Add Skyfire instructions to description if not already present if (!clonedWrap.tool.description.includes(SKYFIRE_TOOL_INSTRUCTIONS)) { clonedWrap.tool.description += `\n\n${SKYFIRE_TOOL_INSTRUCTIONS}`; @@ -287,7 +285,7 @@ export class ActorsMcpServer { }; } } - + // Store the cloned and modified tool this.tools.set(clonedWrap.tool.name, clonedWrap); } else { diff --git a/src/utils/tools.ts b/src/utils/tools.ts index 2772adbc..4356340a 100644 --- a/src/utils/tools.ts +++ b/src/utils/tools.ts @@ -1,5 +1,5 @@ import { toolCategories } from '../tools/index.js'; -import type { ToolBase, ToolCategory, ToolEntry } from '../types.js'; +import type { InternalTool, ToolBase, ToolCategory, ToolEntry } from '../types.js'; /** * Returns a public version of the tool containing only fields that should be exposed publicly. @@ -35,7 +35,7 @@ export function getExpectedToolNamesByCategories(categories: ToolCategory[]): st export function cloneToolEntry(toolEntry: ToolEntry): ToolEntry { // Store the original functions const originalAjvValidate = toolEntry.tool.ajvValidate; - const originalCall = (toolEntry.tool as any).call; + const originalCall = toolEntry.type === 'internal' ? (toolEntry.tool as InternalTool).call : undefined; // Create a deep copy using JSON serialization (excluding functions) const cloned = JSON.parse(JSON.stringify(toolEntry, (key, value) => { @@ -46,7 +46,7 @@ export function cloneToolEntry(toolEntry: ToolEntry): ToolEntry { // Restore the original functions cloned.tool.ajvValidate = originalAjvValidate; if (toolEntry.type === 'internal' && originalCall) { - (cloned.tool as any).call = originalCall; + (cloned.tool as InternalTool).call = originalCall; } return cloned;