diff --git a/src/tools/actor.ts b/src/tools/actor.ts index 579e7aea..1732673b 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -407,7 +407,7 @@ EXAMPLES: } const toolsResponse = await client.listTools(); - const toolsInfo = toolsResponse.tools.map((tool) => `**${tool.name}**\n${tool.description || 'No description'}\nInput schema: ${JSON.stringify(tool.inputSchema, null, 2)}`, + const toolsInfo = toolsResponse.tools.map((tool) => `**${tool.name}**\n${tool.description || 'No description'}\nInput schema:\n\`\`\`json\n${JSON.stringify(tool.inputSchema)}\n\`\`\``, ).join('\n\n'); return buildMCPResponse([`This is an MCP Server Actor with the following tools:\n\n${toolsInfo}\n\nTo call a tool, use step="call" with actor name format: "${baseActorName}:{toolName}"`]); @@ -422,7 +422,7 @@ EXAMPLES: } const content = [ `Actor name: ${actorName}`, - `Input schema: \n${JSON.stringify(details.inputSchema, null, 0)}`, + `Input schema:\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\``, `To run Actor, use step="call" with Actor name format: "${actorName}"`, ]; // Add Skyfire instructions also in the info performStep since clients are most likely truncating @@ -504,7 +504,7 @@ EXAMPLES: const { errors } = actor.tool.ajvValidate; const content = [ `Input validation failed for Actor '${actorName}'. Please ensure your input matches the Actor's input schema.`, - `Input schema:\n${JSON.stringify(actor.tool.inputSchema)}`, + `Input schema:\n\`\`\`json\n${JSON.stringify(actor.tool.inputSchema)}\n\`\`\``, ]; if (errors && errors.length > 0) { content.push(`Validation errors: ${errors.map((e) => e.message).join(', ')}`); diff --git a/src/tools/build.ts b/src/tools/build.ts index be6044db..f9b577fd 100644 --- a/src/tools/build.ts +++ b/src/tools/build.ts @@ -131,7 +131,7 @@ export const actorDefinitionTool: ToolEntry = { const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties }); v.input.properties = shortenProperties(properties); } - return { content: [{ type: 'text', text: JSON.stringify(v) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; }, } as InternalTool, }; diff --git a/src/tools/dataset.ts b/src/tools/dataset.ts index 9c1aa961..a72ce760 100644 --- a/src/tools/dataset.ts +++ b/src/tools/dataset.ts @@ -67,7 +67,7 @@ USAGE EXAMPLES: if (!v) { return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }] }; } - return { content: [{ type: 'text', text: JSON.stringify(v) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; }, } as InternalTool, }; @@ -116,7 +116,7 @@ USAGE EXAMPLES: if (!v) { return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }] }; } - return { content: [{ type: 'text', text: JSON.stringify(v) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; }, } as InternalTool, }; @@ -191,7 +191,7 @@ USAGE EXAMPLES: return { content: [{ type: 'text', - text: JSON.stringify(schema), + text: `\`\`\`json\n${JSON.stringify(schema)}\n\`\`\``, }], }; }, diff --git a/src/tools/dataset_collection.ts b/src/tools/dataset_collection.ts index fb172274..2f29f553 100644 --- a/src/tools/dataset_collection.ts +++ b/src/tools/dataset_collection.ts @@ -54,7 +54,7 @@ USAGE EXAMPLES: desc: parsed.desc, unnamed: parsed.unnamed, }); - return { content: [{ type: 'text', text: JSON.stringify(datasets) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(datasets)}\n\`\`\`` }] }; }, } as InternalTool, }; diff --git a/src/tools/fetch-actor-details.ts b/src/tools/fetch-actor-details.ts index 90d7178f..392e2237 100644 --- a/src/tools/fetch-actor-details.ts +++ b/src/tools/fetch-actor-details.ts @@ -52,7 +52,7 @@ USAGE EXAMPLES: // Include input schema if it has properties if (details.inputSchema.properties || Object.keys(details.inputSchema.properties).length !== 0) { - content.push({ type: 'text', text: `# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema, null, 0)}\n\`\`\`` }); + content.push({ type: 'text', text: `# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\`` }); } // Return the actor card, README, and input schema (if it has non-empty properties) as separate text blocks // This allows better formatting in the final output diff --git a/src/tools/get-actor-output.ts b/src/tools/get-actor-output.ts index a14d9cbc..ba4aea81 100644 --- a/src/tools/get-actor-output.ts +++ b/src/tools/get-actor-output.ts @@ -147,7 +147,7 @@ Note: This tool is automatically included if the Apify MCP Server is configured .map((item) => cleanEmptyProperties(item)) .filter((item) => item !== undefined); - let outputText = JSON.stringify(cleanedItems); + let outputText = `\`\`\`json\n${JSON.stringify(cleanedItems)}\n\`\`\``; let truncated = false; if (outputText.length > TOOL_MAX_OUTPUT_CHARS) { outputText = outputText.slice(0, TOOL_MAX_OUTPUT_CHARS); diff --git a/src/tools/key_value_store.ts b/src/tools/key_value_store.ts index 037f87bd..eb9124e5 100644 --- a/src/tools/key_value_store.ts +++ b/src/tools/key_value_store.ts @@ -36,7 +36,7 @@ USAGE EXAMPLES: const parsed = getKeyValueStoreArgs.parse(args); const client = new ApifyClient({ token: apifyToken }); const store = await client.keyValueStore(parsed.storeId).get(); - return { content: [{ type: 'text', text: JSON.stringify(store) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(store)}\n\`\`\`` }] }; }, } as InternalTool, }; @@ -82,7 +82,7 @@ USAGE EXAMPLES: exclusiveStartKey: parsed.exclusiveStartKey, limit: parsed.limit, }); - return { content: [{ type: 'text', text: JSON.stringify(keys) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(keys)}\n\`\`\`` }] }; }, } as InternalTool, }; @@ -120,7 +120,7 @@ USAGE EXAMPLES: const parsed = getKeyValueStoreRecordArgs.parse(args); const client = new ApifyClient({ token: apifyToken }); const record = await client.keyValueStore(parsed.storeId).getRecord(parsed.recordKey); - return { content: [{ type: 'text', text: JSON.stringify(record) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(record)}\n\`\`\`` }] }; }, } as InternalTool, }; diff --git a/src/tools/key_value_store_collection.ts b/src/tools/key_value_store_collection.ts index 7f266c0f..c62ed1ac 100644 --- a/src/tools/key_value_store_collection.ts +++ b/src/tools/key_value_store_collection.ts @@ -54,7 +54,7 @@ USAGE EXAMPLES: desc: parsed.desc, unnamed: parsed.unnamed, }); - return { content: [{ type: 'text', text: JSON.stringify(stores) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(stores)}\n\`\`\`` }] }; }, } as InternalTool, }; diff --git a/src/tools/run.ts b/src/tools/run.ts index 6a053c51..1261b909 100644 --- a/src/tools/run.ts +++ b/src/tools/run.ts @@ -46,7 +46,7 @@ USAGE EXAMPLES: if (!v) { return { content: [{ type: 'text', text: `Run with ID '${parsed.runId}' not found.` }] }; } - return { content: [{ type: 'text', text: JSON.stringify(v) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; }, } as InternalTool, }; @@ -116,7 +116,7 @@ USAGE EXAMPLES: const parsed = abortRunArgs.parse(args); const client = new ApifyClient({ token: apifyToken }); const v = await client.run(parsed.runId).abort({ gracefully: parsed.gracefully }); - return { content: [{ type: 'text', text: JSON.stringify(v) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; }, } as InternalTool, }; diff --git a/src/tools/run_collection.ts b/src/tools/run_collection.ts index 59abfeb2..7e211f0c 100644 --- a/src/tools/run_collection.ts +++ b/src/tools/run_collection.ts @@ -47,7 +47,7 @@ USAGE EXAMPLES: const parsed = getUserRunsListArgs.parse(args); const client = new ApifyClient({ token: apifyToken }); const runs = await client.runs().list({ limit: parsed.limit, offset: parsed.offset, desc: parsed.desc, status: parsed.status }); - return { content: [{ type: 'text', text: JSON.stringify(runs) }] }; + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(runs)}\n\`\`\`` }] }; }, } as InternalTool, }; diff --git a/src/utils/actor-response.ts b/src/utils/actor-response.ts index fe998655..f2bf1c06 100644 --- a/src/utils/actor-response.ts +++ b/src/utils/actor-response.ts @@ -38,7 +38,7 @@ Results summary: Actor output data schema: * You can use this schema to understand the structure of the output data and, for example, retrieve specific fields based on your current task. \`\`\`json -${JSON.stringify(displaySchema, null, 2)} +${JSON.stringify(displaySchema)} \`\`\` Above this text block is a preview of the Actor output containing ${result.previewItems.length} item(s).${itemCount !== result.previewItems.length ? ` You have access only to a limited preview of the Actor output. Do not present this as the full output, as you have only ${result.previewItems.length} item(s) available instead of the full ${itemCount} item(s). Be aware of this and inform users about the currently loaded count and the total available output items count.` : ''} diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index 21dcfcc4..22eece0f 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -31,6 +31,17 @@ function expectToolNamesToContain(names: string[], toolNames: string[] = []) { toolNames.forEach((name) => expect(names).toContain(name)); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractJsonFromMarkdown(text: string): any { + // Handle markdown code blocks like ```json + const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); + if (jsonMatch) { + return JSON.parse(jsonMatch[1]); + } + // If no markdown formatting, assume it's raw JSON + return JSON.parse(text); +} + async function callPythonExampleActor(client: Client, selectedToolName: string) { const result = await client.callTool({ name: selectedToolName, @@ -53,7 +64,7 @@ async function callPythonExampleActor(client: Client, selectedToolName: string) }; // Parse the JSON to compare objects regardless of property order const actual = content[0]; - expect(JSON.parse(actual.text)).toEqual(JSON.parse(expected.text)); + expect(extractJsonFromMarkdown(actual.text)).toEqual(JSON.parse(expected.text)); expect(actual.type).toBe(expected.type); } @@ -836,7 +847,7 @@ export function createIntegrationTestsSuite( expect(outputResult.content).toBeDefined(); const outputContent = outputResult.content as { text: string; type: string }[]; - const output = JSON.parse(outputContent[0].text); + const output = extractJsonFromMarkdown(outputContent[0].text); expect(Array.isArray(output)).toBe(true); expect(output.length).toBeGreaterThan(0); expect(output[0]).toHaveProperty('metadata.title'); @@ -894,7 +905,7 @@ export function createIntegrationTestsSuite( // Validate the output contains the expected structure with metadata.title expect(outputResult.content).toBeDefined(); const outputContent = outputResult.content as { text: string; type: string }[]; - const output = JSON.parse(outputContent[0].text); + const output = extractJsonFromMarkdown(outputContent[0].text); expect(Array.isArray(output)).toBe(true); expect(output.length).toBeGreaterThan(0); expect(output[0]).toHaveProperty('metadata.title'); @@ -935,7 +946,7 @@ export function createIntegrationTestsSuite( expect(outputResult.content).toBeDefined(); const outputContent = outputResult.content as { text: string; type: string }[]; - const output = JSON.parse(outputContent[0].text); + const output = extractJsonFromMarkdown(outputContent[0].text); expect(Array.isArray(output)).toBe(true); expect(output.length).toBe(1); expect(output[0]).toHaveProperty('first_number', input.first_number);