From 4d36a9eb7862bb0ae42f0774aa8624103384532b Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Jul 2025 15:56:56 +0200 Subject: [PATCH 1/3] encode dots by replacing with '-dot-' --- src/mcp/server.ts | 4 +++- src/tools/actor.ts | 2 ++ src/tools/utils.ts | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 00d4e651..bacf2de6 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -24,7 +24,7 @@ import { SERVER_VERSION, } from '../const.js'; import { addRemoveTools, betaTools, callActorGetDataset, defaultTools, getActorsAsTools } from '../tools/index.js'; -import { actorNameToToolName } from '../tools/utils.js'; +import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js'; import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js'; import { connectMCPClient } from './client.js'; import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC } from './const.js'; @@ -407,6 +407,8 @@ export class ActorsMcpServer { msg, ); } + // Decode dot property names in arguments + args = decodeDotPropertyNames(args); log.info(`Validate arguments for tool: ${tool.tool.name} with arguments: ${JSON.stringify(args)}`); if (!tool.tool.ajvValidate(args)) { const msg = `Invalid arguments for tool ${tool.tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.tool.ajvValidate.errors)}`; diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 25a07d21..5d3e9b5e 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -24,6 +24,7 @@ import { actorNameToToolName, addEnumsToDescriptionsWithExamples, buildNestedProperties, + encodeDotPropertyNames, filterSchemaProperties, fixedAjvCompile, getToolSchemaID, @@ -128,6 +129,7 @@ export async function getNormalActorsAsTools( actorDefinitionPruned.input.properties = filterSchemaProperties(actorDefinitionPruned.input.properties); actorDefinitionPruned.input.properties = shortenProperties(actorDefinitionPruned.input.properties); actorDefinitionPruned.input.properties = addEnumsToDescriptionsWithExamples(actorDefinitionPruned.input.properties); + actorDefinitionPruned.input.properties = encodeDotPropertyNames(actorDefinitionPruned.input.properties); // Add schema $id, each valid JSON schema should have a unique $id // see https://json-schema.org/understanding-json-schema/basics#declaring-a-unique-identifier actorDefinitionPruned.input.$id = schemaID; diff --git a/src/tools/utils.ts b/src/tools/utils.ts index bb191354..6cae126b 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -242,3 +242,40 @@ export function shortenProperties(properties: { [key: string]: ISchemaProperties return properties; } + +/** + * Fixes dot notation in the property names of schema properties. + * + * Some providers, such as Anthropic, allow only the following characters in property names: `^[a-zA-Z0-9_-]{1,64}$`. + * + * @param properties - The schema properties to fix. + * @returns {Record} The schema properties with fixed names. + */ +export function encodeDotPropertyNames(properties: Record): Record { + const encodedProperties: Record = {}; + for (const [key, value] of Object.entries(properties)) { + // Replace dots with '-dot-' to avoid issues with property names + const fixedKey = key.replace(/\./g, '-dot-'); + encodedProperties[fixedKey] = value; + } + return encodedProperties; +} + +/** + * Restores original property names by replacing '-dot-' with '.'. + * + * This is necessary to decode the property names that were encoded to avoid issues with providers + * that do not allow dots in property names. + * + * @param properties - The schema properties with encoded names. + * @returns {Record} The schema properties with restored names. + */ +export function decodeDotPropertyNames(properties: Record): Record { + const decodedProperties: Record = {}; + for (const [key, value] of Object.entries(properties)) { + // Replace '-dot-' with '.' to restore original property names + const decodedKey = key.replace(/-dot-/g, '.'); + decodedProperties[decodedKey] = value; + } + return decodedProperties; +} From 25a6c19029b278f6607006e055421b6512738a23 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Jul 2025 16:19:45 +0200 Subject: [PATCH 2/3] comment --- src/mcp/server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index bacf2de6..75f00a4e 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -407,7 +407,8 @@ export class ActorsMcpServer { msg, ); } - // Decode dot property names in arguments + // Decode dot property names in arguments before validation, + // since validation expects the original, non-encoded property names. args = decodeDotPropertyNames(args); log.info(`Validate arguments for tool: ${tool.tool.name} with arguments: ${JSON.stringify(args)}`); if (!tool.tool.ajvValidate(args)) { From 682374a5555112e1543ad24f6e5e80327e3e9df9 Mon Sep 17 00:00:00 2001 From: MQ Date: Tue, 15 Jul 2025 16:51:48 +0200 Subject: [PATCH 3/3] add unit tests --- tests/unit/tools.utils.test.ts | 53 +++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/unit/tools.utils.test.ts b/tests/unit/tools.utils.test.ts index 3129389f..0903dd6f 100644 --- a/tests/unit/tools.utils.test.ts +++ b/tests/unit/tools.utils.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest'; import { ACTOR_ENUM_MAX_LENGTH, ACTOR_MAX_DESCRIPTION_LENGTH } from '../../src/const.js'; -import { buildNestedProperties, markInputPropertiesAsRequired, shortenProperties } from '../../src/tools/utils.js'; +import { buildNestedProperties, decodeDotPropertyNames, encodeDotPropertyNames, + markInputPropertiesAsRequired, shortenProperties } from '../../src/tools/utils.js'; import type { IActorInputSchema, ISchemaProperties } from '../../src/types.js'; describe('buildNestedProperties', () => { @@ -317,3 +318,53 @@ describe('shortenProperties', () => { expect(result).toEqual(properties); }); }); + +describe('encodeDotPropertyNames', () => { + it('should replace dots in property names with -dot-', () => { + const input = { + 'foo.bar': { type: 'string', title: 'Foo Bar', description: 'desc' }, + baz: { type: 'number', title: 'Baz', description: 'desc2' }, + 'a.b.c': { type: 'boolean', title: 'A B C', description: 'desc3' }, + }; + const result = encodeDotPropertyNames(input); + expect(result['foo-dot-bar']).toBeDefined(); + expect(result['a-dot-b-dot-c']).toBeDefined(); + expect(result.baz).toBeDefined(); + expect(result['foo.bar']).toBeUndefined(); + expect(result['a.b.c']).toBeUndefined(); + }); + + it('should not modify property names without dots', () => { + const input = { + foo: { type: 'string', title: 'Foo', description: 'desc' }, + bar: { type: 'number', title: 'Bar', description: 'desc2' }, + }; + const result = encodeDotPropertyNames(input); + expect(result).toEqual(input); + }); +}); + +describe('decodeDotPropertyNames', () => { + it('should replace -dot- in property names with dots', () => { + const input = { + 'foo-dot-bar': { type: 'string', title: 'Foo Bar', description: 'desc' }, + baz: { type: 'number', title: 'Baz', description: 'desc2' }, + 'a-dot-b-dot-c': { type: 'boolean', title: 'A B C', description: 'desc3' }, + }; + const result = decodeDotPropertyNames(input); + expect(result['foo.bar']).toBeDefined(); + expect(result['a.b.c']).toBeDefined(); + expect(result.baz).toBeDefined(); + expect(result['foo-dot-bar']).toBeUndefined(); + expect(result['a-dot-b-dot-c']).toBeUndefined(); + }); + + it('should not modify property names without -dot-', () => { + const input = { + foo: { type: 'string', title: 'Foo', description: 'desc' }, + bar: { type: 'number', title: 'Bar', description: 'desc2' }, + }; + const result = decodeDotPropertyNames(input); + expect(result).toEqual(input); + }); +});