Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
122 changes: 107 additions & 15 deletions src/actors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ApifyClient } from 'apify-client';

import { ACTOR_ADDITIONAL_INSTRUCTIONS, defaults, MAX_DESCRIPTION_LENGTH, ACTOR_README_MAX_LENGTH } from './const.js';
import { log } from './logger.js';
import type { ActorDefinitionPruned, ActorDefinitionWithDesc, SchemaProperties, Tool } from './types.js';
import type { ActorDefinitionPruned, ActorDefinitionWithDesc, IActorInputSchema, ISchemaProperties, Tool } from './types.js';

export function actorNameToToolName(actorName: string): string {
return actorName
Expand Down Expand Up @@ -67,7 +67,11 @@ function pruneActorDefinition(response: ActorDefinitionWithDesc): ActorDefinitio
actorFullName: response.actorFullName || '',
buildTag: response?.buildTag || '',
readme: response?.readme || '',
input: response?.input || null,
input: response?.input && 'type' in response.input && 'properties' in response.input
? { ...response.input,
type: response.input.type as string,
properties: response.input.properties as Record<string, ISchemaProperties> }
: undefined,
description: response.description,
defaultRunOptions: response.defaultRunOptions,
};
Expand All @@ -77,7 +81,7 @@ function pruneActorDefinition(response: ActorDefinitionWithDesc): ActorDefinitio
* Shortens the description and enum values of schema properties.
* @param properties
*/
export function shortenProperties(properties: { [key: string]: SchemaProperties}): { [key: string]: SchemaProperties } {
export function shortenProperties(properties: { [key: string]: ISchemaProperties}): { [key: string]: ISchemaProperties } {
for (const property of Object.values(properties)) {
if (property.description.length > MAX_DESCRIPTION_LENGTH) {
property.description = `${property.description.slice(0, MAX_DESCRIPTION_LENGTH)}...`;
Expand Down Expand Up @@ -106,17 +110,19 @@ export function truncateActorReadme(readme: string, limit = ACTOR_README_MAX_LEN
* Helps determine the type of items in an array schema property.
* Priority order: explicit type in items > prefill type > default value type > editor type.
*/
export function inferArrayItemType(property: SchemaProperties): string | null {
export function inferArrayItemType(property: ISchemaProperties): string | null {
return property.items?.type
|| (property.prefill && typeof property.prefill[0])
|| (property.default && typeof property.default[0])
|| (Array.isArray(property.prefill) && property.prefill.length > 0 && typeof property.prefill[0])
|| (Array.isArray(property.default) && property.default.length > 0 && typeof property.default[0])
|| (property.editor && getEditorItemType(property.editor))
|| null;

function getEditorItemType(editor: string): string | null {
const editorTypeMap: Record<string, string> = {
requestListSources: 'object',
stringList: 'string',
json: 'object',
globs: 'object',
};
return editorTypeMap[editor] || null;
}
Expand All @@ -126,14 +132,15 @@ export function inferArrayItemType(property: SchemaProperties): string | null {
* Add enum values as string to property descriptions.
* @param properties
*/
export function addEnumsToDescriptionsWithExamples(properties: { [key: string]: SchemaProperties }): { [key: string]: SchemaProperties } {
function addEnumsToDescriptionsWithExamples(properties: { [key: string]: ISchemaProperties }): { [key: string]: ISchemaProperties } {
for (const property of Object.values(properties)) {
if (property.enum && property.enum.length > 0) {
property.description = `${property.description}\nPossible values: ${property.enum.join(',')}`;
}
const value = property.prefill ?? property.default;
if (value && !(Array.isArray(value) && value.length === 0)) {
property.examples = Array.isArray(value) ? value : [value];
property.description = `${property.description}\nExample values: ${JSON.stringify(value)}`;
}
}
return properties;
Expand All @@ -143,21 +150,104 @@ export function addEnumsToDescriptionsWithExamples(properties: { [key: string]:
* Filters schema properties to include only the necessary fields.
* @param properties
*/
export function filterSchemaProperties(properties: { [key: string]: SchemaProperties }): { [key: string]: SchemaProperties } {
const filteredProperties: { [key: string]: SchemaProperties } = {};
export function filterSchemaProperties(properties: { [key: string]: ISchemaProperties }): { [key: string]: ISchemaProperties } {
const filteredProperties: { [key: string]: ISchemaProperties } = {};
for (const [key, property] of Object.entries(properties)) {
const { title, description, enum: enumValues, type, default: defaultValue, prefill } = property;
filteredProperties[key] = { title, description, enum: enumValues, type, default: defaultValue, prefill };
if (type === 'array') {
const { title, description, enum: enumValues, type,
default: defaultValue, prefill, properties: subProperties,
items, required } = property;
filteredProperties[key] = { title,
description,
enum: enumValues,
type,
default: defaultValue,
prefill,
properties: subProperties,
items,
required };
if (type === 'array' && !items?.type) {
const itemsType = inferArrayItemType(property);
if (itemsType) {
filteredProperties[key].items = { type: itemsType };
filteredProperties[key].items = {
...filteredProperties[key].items,
title: filteredProperties[key].title ?? 'Item',
description: filteredProperties[key].description ?? 'Item',
type: itemsType,
};
}
}
}
return filteredProperties;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be nicer like this?

I haven't tested it, only draft.

export function filterSchemaProperties(properties: { [key: string]: ISchemaProperties }): { [key: string]: ISchemaProperties } {
    const filteredProperties: { [key: string]: ISchemaProperties } = {};
    for (const [key, property] of Object.entries(properties)) {
        filteredProperties[key] = {
            title: property.title,
            description: property.description,
            enum: property.enum,
            type: property.type,
            default: property.default,
            prefill: property.prefill,
            properties: property.properties,
            items: property.items,
            required: property.required,
        };
        if (property.type === 'array' && !property.items?.type) {
            const itemsType = inferArrayItemType(property);
            if (itemsType) {
                filteredProperties[key].items = {
                    ...filteredProperties[key].items,
                    title: filteredProperties[key].title ?? 'Item',
                    description: filteredProperties[key].description ?? 'Item',
                    type: itemsType,
                };
            }
        }
    }
    return filteredProperties;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jirispilka yes, I haven't refactored the original code, but I think once we have a fully functional version a refactor would be great 👍


/**
* Marks input properties as required by adding a "REQUIRED" prefix to their descriptions.
* Takes an IActorInput object and returns a modified Record of SchemaProperties.
* @param {IActorInputSchema} input - Actor input object containing properties and required fields
* @returns {Record<string, ISchemaProperties>} - Modified properties with required fields marked
*/
function markInputPropertiesAsRequired(input: IActorInputSchema): Record<string, ISchemaProperties> {
const { required = [], properties } = input;

for (const property of Object.keys(properties)) {
if (required.includes(property)) {
properties[property] = {
...properties[property],
description: `**REQUIRED** ${properties[property].description}`,
};
}
}

return properties;
}

/**
* Builds nested properties for object types in the schema.
* @param {Record<string, ISchemaProperties>} properties - The input schema properties
* @returns {Record<string, ISchemaProperties>} Modified properties with nested properties
*/
function buildNestedProperties(properties: Record<string, ISchemaProperties>): Record<string, ISchemaProperties> {
const clonedProperties = { ...properties };

for (const [propertyName, property] of Object.entries(clonedProperties)) {
if (property.type === 'object' && property.editor === 'proxy') {
clonedProperties[propertyName] = {
...property,
properties: {
...property.properties,
useApifyProxy: {
title: 'Use Apify Proxy',
type: 'boolean',
description: 'Whether to use Apify Proxy - ALWAYS SET TO TRUE.',
default: true,
examples: [true],
},
},
required: ['useApifyProxy'],
};
} else if (property.type === 'array' && property.editor === 'requestListSources') {
clonedProperties[propertyName] = {
...property,
items: {
...property.items,
type: 'object',
title: 'Request list source',
description: 'Request list source',
properties: {
url: {
title: 'URL',
type: 'string',
description: 'URL of the request list source',
},
},
},
};
}
}

return clonedProperties;
}

/**
* Fetches actor input schemas by Actor IDs or Actor full names and creates MCP tools.
*
Expand All @@ -176,8 +266,10 @@ export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
for (const result of results) {
if (result) {
if (result.input && 'properties' in result.input && result.input) {
const properties = filterSchemaProperties(result.input.properties as { [key: string]: SchemaProperties });
const propertiesShortened = shortenProperties(properties);
const propertiesMarkedAsRequired = markInputPropertiesAsRequired(result.input);
const propertiesObjectsBuilt = buildNestedProperties(propertiesMarkedAsRequired);
const propertiesFiltered = filterSchemaProperties(propertiesObjectsBuilt);
const propertiesShortened = shortenProperties(propertiesFiltered);
result.input.properties = addEnumsToDescriptionsWithExamples(propertiesShortened);
}
try {
Expand Down
4 changes: 2 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
searchActorsByKeywords,
GetActorDefinition,
} from './tools.js';
import type { SchemaProperties, Tool } from './types.js';
import type { ISchemaProperties, Tool } from './types.js';

/**
* Create Apify MCP server
Expand Down Expand Up @@ -199,7 +199,7 @@ export class ApifyMcpServer {
const parsed = GetActorDefinition.parse(args);
const v = await getActorDefinition(parsed.actorName, parsed.limit);
if (v && v.input && 'properties' in v.input && v.input) {
const properties = filterSchemaProperties(v.input.properties as { [key: string]: SchemaProperties });
const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties });
v.input.properties = shortenProperties(properties);
}
return { content: [{ type: 'text', text: JSON.stringify(v) }] };
Expand Down
56 changes: 34 additions & 22 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,48 @@ export type Input = {
debugActorInput?: unknown;
};

export interface ActorDefinitionPruned {
id: string;
actorFullName: string;
buildTag?: string;
readme?: string | null;
input?: object | null;
export interface ISchemaProperties {
type: string;

title: string;
description: string;
defaultRunOptions: ActorDefaultRunOptions;

enum?: string[]; // Array of string options for the enum
enumTitles?: string[]; // Array of string titles for the enum
default?: unknown;
prefill?: unknown;

items?: ISchemaProperties;
editor?: string;
examples?: unknown[];

properties?: Record<string, ISchemaProperties>;
required?: string[];
}

export interface ActorDefinitionWithDesc extends ActorDefinition {
export interface IActorInputSchema {
title?: string;
description?: string;

type: string;

properties: Record<string, ISchemaProperties>;

required?: string[];
schemaVersion?: number;
}

export type ActorDefinitionWithDesc = Omit<ActorDefinition, 'input'> & {
id: string;
actorFullName: string;
description: string;
defaultRunOptions: ActorDefaultRunOptions
defaultRunOptions: ActorDefaultRunOptions;
input?: IActorInputSchema;
}

export type ActorDefinitionPruned = Pick<ActorDefinitionWithDesc,
'id' | 'actorFullName' | 'buildTag' | 'readme' | 'input' | 'description' | 'defaultRunOptions'>

export interface Tool {
name: string;
actorFullName: string;
Expand All @@ -35,19 +60,6 @@ export interface Tool {
memoryMbytes?: number;
}

export interface SchemaProperties {
title: string;
description: string;
enum: string[]; // Array of string options for the enum
enumTitles?: string[]; // Array of string titles for the enum
type: string; // Data type (e.g., "string")
default: string;
prefill: string;
items?: { type: string; }
editor?: string;
examples?: unknown[];
}

// ActorStoreList for actor-search tool
export interface ActorStats {
totalRuns: number;
Expand Down
Loading