Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -407,6 +407,9 @@ export class ActorsMcpServer {
msg,
);
}
// 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)) {
const msg = `Invalid arguments for tool ${tool.tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.tool.ajvValidate.errors)}`;
Expand Down
2 changes: 2 additions & 0 deletions src/tools/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
actorNameToToolName,
addEnumsToDescriptionsWithExamples,
buildNestedProperties,
encodeDotPropertyNames,
filterSchemaProperties,
fixedAjvCompile,
getToolSchemaID,
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ISchemaProperties>} The schema properties with fixed names.
*/
export function encodeDotPropertyNames(properties: Record<string, ISchemaProperties>): Record<string, ISchemaProperties> {
const encodedProperties: Record<string, ISchemaProperties> = {};
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<string, ISchemaProperties>} The schema properties with restored names.
*/
export function decodeDotPropertyNames(properties: Record<string, unknown>): Record<string, unknown> {
const decodedProperties: Record<string, unknown> = {};
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;
}
53 changes: 52 additions & 1 deletion tests/unit/tools.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});
});