Skip to content

Commit 8dbdc7b

Browse files
authored
fix: encode Actor input fields containing dots (#170)
* encode dots by replacing with '-dot-' * comment * add unit tests
1 parent 283d279 commit 8dbdc7b

File tree

4 files changed

+95
-2
lines changed

4 files changed

+95
-2
lines changed

src/mcp/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
SERVER_VERSION,
2525
} from '../const.js';
2626
import { addRemoveTools, betaTools, callActorGetDataset, defaultTools, getActorsAsTools } from '../tools/index.js';
27-
import { actorNameToToolName } from '../tools/utils.js';
27+
import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js';
2828
import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js';
2929
import { connectMCPClient } from './client.js';
3030
import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC } from './const.js';
@@ -407,6 +407,9 @@ export class ActorsMcpServer {
407407
msg,
408408
);
409409
}
410+
// Decode dot property names in arguments before validation,
411+
// since validation expects the original, non-encoded property names.
412+
args = decodeDotPropertyNames(args);
410413
log.info(`Validate arguments for tool: ${tool.tool.name} with arguments: ${JSON.stringify(args)}`);
411414
if (!tool.tool.ajvValidate(args)) {
412415
const msg = `Invalid arguments for tool ${tool.tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.tool.ajvValidate.errors)}`;

src/tools/actor.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
actorNameToToolName,
2525
addEnumsToDescriptionsWithExamples,
2626
buildNestedProperties,
27+
encodeDotPropertyNames,
2728
filterSchemaProperties,
2829
fixedAjvCompile,
2930
getToolSchemaID,
@@ -128,6 +129,7 @@ export async function getNormalActorsAsTools(
128129
actorDefinitionPruned.input.properties = filterSchemaProperties(actorDefinitionPruned.input.properties);
129130
actorDefinitionPruned.input.properties = shortenProperties(actorDefinitionPruned.input.properties);
130131
actorDefinitionPruned.input.properties = addEnumsToDescriptionsWithExamples(actorDefinitionPruned.input.properties);
132+
actorDefinitionPruned.input.properties = encodeDotPropertyNames(actorDefinitionPruned.input.properties);
131133
// Add schema $id, each valid JSON schema should have a unique $id
132134
// see https://json-schema.org/understanding-json-schema/basics#declaring-a-unique-identifier
133135
actorDefinitionPruned.input.$id = schemaID;

src/tools/utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,40 @@ export function shortenProperties(properties: { [key: string]: ISchemaProperties
242242

243243
return properties;
244244
}
245+
246+
/**
247+
* Fixes dot notation in the property names of schema properties.
248+
*
249+
* Some providers, such as Anthropic, allow only the following characters in property names: `^[a-zA-Z0-9_-]{1,64}$`.
250+
*
251+
* @param properties - The schema properties to fix.
252+
* @returns {Record<string, ISchemaProperties>} The schema properties with fixed names.
253+
*/
254+
export function encodeDotPropertyNames(properties: Record<string, ISchemaProperties>): Record<string, ISchemaProperties> {
255+
const encodedProperties: Record<string, ISchemaProperties> = {};
256+
for (const [key, value] of Object.entries(properties)) {
257+
// Replace dots with '-dot-' to avoid issues with property names
258+
const fixedKey = key.replace(/\./g, '-dot-');
259+
encodedProperties[fixedKey] = value;
260+
}
261+
return encodedProperties;
262+
}
263+
264+
/**
265+
* Restores original property names by replacing '-dot-' with '.'.
266+
*
267+
* This is necessary to decode the property names that were encoded to avoid issues with providers
268+
* that do not allow dots in property names.
269+
*
270+
* @param properties - The schema properties with encoded names.
271+
* @returns {Record<string, ISchemaProperties>} The schema properties with restored names.
272+
*/
273+
export function decodeDotPropertyNames(properties: Record<string, unknown>): Record<string, unknown> {
274+
const decodedProperties: Record<string, unknown> = {};
275+
for (const [key, value] of Object.entries(properties)) {
276+
// Replace '-dot-' with '.' to restore original property names
277+
const decodedKey = key.replace(/-dot-/g, '.');
278+
decodedProperties[decodedKey] = value;
279+
}
280+
return decodedProperties;
281+
}

tests/unit/tools.utils.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, expect, it } from 'vitest';
22

33
import { ACTOR_ENUM_MAX_LENGTH, ACTOR_MAX_DESCRIPTION_LENGTH } from '../../src/const.js';
4-
import { buildNestedProperties, markInputPropertiesAsRequired, shortenProperties } from '../../src/tools/utils.js';
4+
import { buildNestedProperties, decodeDotPropertyNames, encodeDotPropertyNames,
5+
markInputPropertiesAsRequired, shortenProperties } from '../../src/tools/utils.js';
56
import type { IActorInputSchema, ISchemaProperties } from '../../src/types.js';
67

78
describe('buildNestedProperties', () => {
@@ -317,3 +318,53 @@ describe('shortenProperties', () => {
317318
expect(result).toEqual(properties);
318319
});
319320
});
321+
322+
describe('encodeDotPropertyNames', () => {
323+
it('should replace dots in property names with -dot-', () => {
324+
const input = {
325+
'foo.bar': { type: 'string', title: 'Foo Bar', description: 'desc' },
326+
baz: { type: 'number', title: 'Baz', description: 'desc2' },
327+
'a.b.c': { type: 'boolean', title: 'A B C', description: 'desc3' },
328+
};
329+
const result = encodeDotPropertyNames(input);
330+
expect(result['foo-dot-bar']).toBeDefined();
331+
expect(result['a-dot-b-dot-c']).toBeDefined();
332+
expect(result.baz).toBeDefined();
333+
expect(result['foo.bar']).toBeUndefined();
334+
expect(result['a.b.c']).toBeUndefined();
335+
});
336+
337+
it('should not modify property names without dots', () => {
338+
const input = {
339+
foo: { type: 'string', title: 'Foo', description: 'desc' },
340+
bar: { type: 'number', title: 'Bar', description: 'desc2' },
341+
};
342+
const result = encodeDotPropertyNames(input);
343+
expect(result).toEqual(input);
344+
});
345+
});
346+
347+
describe('decodeDotPropertyNames', () => {
348+
it('should replace -dot- in property names with dots', () => {
349+
const input = {
350+
'foo-dot-bar': { type: 'string', title: 'Foo Bar', description: 'desc' },
351+
baz: { type: 'number', title: 'Baz', description: 'desc2' },
352+
'a-dot-b-dot-c': { type: 'boolean', title: 'A B C', description: 'desc3' },
353+
};
354+
const result = decodeDotPropertyNames(input);
355+
expect(result['foo.bar']).toBeDefined();
356+
expect(result['a.b.c']).toBeDefined();
357+
expect(result.baz).toBeDefined();
358+
expect(result['foo-dot-bar']).toBeUndefined();
359+
expect(result['a-dot-b-dot-c']).toBeUndefined();
360+
});
361+
362+
it('should not modify property names without -dot-', () => {
363+
const input = {
364+
foo: { type: 'string', title: 'Foo', description: 'desc' },
365+
bar: { type: 'number', title: 'Bar', description: 'desc2' },
366+
};
367+
const result = decodeDotPropertyNames(input);
368+
expect(result).toEqual(input);
369+
});
370+
});

0 commit comments

Comments
 (0)