import { z } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
import { HelperTools, TOOL_STATUS } from '../const.js';
import { fetchApifyDocsCache } from '../state.js';
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
import { ajv } from '../utils/ajv.js';
import { htmlToMarkdown } from '../utils/html-to-md.js';
import { logHttpError } from '../utils/logging.js';
import { buildMCPResponse } from '../utils/mcp.js';
import { fetchApifyDocsToolOutputSchema } from './structured-output-schemas.js';
const fetchApifyDocsToolArgsSchema = z.object({
url: z.string()
.min(1)
.describe(`URL of the Apify documentation page to fetch. This should be the full URL, including the protocol (e.g., https://docs.apify.com/).`),
});
export const fetchApifyDocsTool: ToolEntry = {
type: 'internal',
name: HelperTools.DOCS_FETCH,
description: `Fetch the full content of an Apify documentation page by its URL.
Use this after finding a relevant page with the ${HelperTools.DOCS_SEARCH} tool.
USAGE:
- Use when you need the complete content of a specific docs page for detailed answers.
USAGE EXAMPLES:
- user_input: Fetch https://docs.apify.com/platform/actors/running#builds
- user_input: Fetch https://docs.apify.com/academy`,
inputSchema: zodToJsonSchema(fetchApifyDocsToolArgsSchema) as ToolInputSchema,
outputSchema: fetchApifyDocsToolOutputSchema,
ajvValidate: ajv.compile(zodToJsonSchema(fetchApifyDocsToolArgsSchema)),
annotations: {
title: 'Fetch Apify docs',
readOnlyHint: true,
openWorldHint: false,
},
call: async (toolArgs: InternalToolArgs) => {
const { args } = toolArgs;
const parsed = fetchApifyDocsToolArgsSchema.parse(args);
const url = parsed.url.trim();
const urlWithoutFragment = url.split('#')[0];
// Only allow URLs starting with https://docs.apify.com
if (!url.startsWith('https://docs.apify.com')) {
return buildMCPResponse({ texts: [`Invalid URL: "${url}".
Only URLs starting with "https://docs.apify.com" are allowed.
Please provide a valid Apify documentation URL. You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`],
isError: true,
toolStatus: TOOL_STATUS.SOFT_FAIL });
}
// Cache URL without fragment to avoid fetching the same page multiple times
let markdown = fetchApifyDocsCache.get(urlWithoutFragment);
// If the content is not cached, fetch it from the URL
if (!markdown) {
try {
const response = await fetch(url);
if (!response.ok) {
const error = Object.assign(new Error(`HTTP ${response.status} ${response.statusText}`), {
statusCode: response.status,
});
logHttpError(error, 'Failed to fetch the documentation page', { url, statusText: response.statusText });
// HTTP 4xx = user error (soft_fail), 5xx = server error (will be caught by catch block)
const isUserError = response.status >= 400 && response.status < 500;
return buildMCPResponse({
texts: [`Failed to fetch the documentation page at "${url}".
HTTP Status: ${response.status} ${response.statusText}.
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`],
isError: true,
toolStatus: isUserError ? TOOL_STATUS.SOFT_FAIL : TOOL_STATUS.FAILED,
});
}
const html = await response.text();
markdown = htmlToMarkdown(html);
// Cache the processed Markdown content
// Use the URL without fragment as the key to avoid caching same page with different fragments
fetchApifyDocsCache.set(urlWithoutFragment, markdown);
} catch (error) {
logHttpError(error, 'Failed to fetch the documentation page', { url });
// Network/fetch errors are typically user errors (bad URL, connectivity issues)
return buildMCPResponse({
texts: [`Failed to fetch the documentation page at "${url}".
Error: ${error instanceof Error ? error.message : String(error)}.
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`],
isError: true,
toolStatus: TOOL_STATUS.SOFT_FAIL,
});
}
}
return buildMCPResponse({ texts: [`Fetched content from ${url}:\n\n${markdown}`], structuredContent: { url, content: markdown } });
},
} as const;