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
64 changes: 58 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,44 @@ function emitActionLog(
}).catch(() => undefined);
}

const AGENT_HINT_LOG_MAX_CHARS = 1000;

/**
* One `[MCP_AGENT_HINTS]` line per tool call that surfaced API agent hints.
* The strings come from the Firecrawl API, never from page content, so they
* are logged verbatim (capped) to count each hint; `request_id` joins the line
* to the call's `[MCP_ACTION]` records for account-level breakdowns.
*/
function emitAgentHintsLog(
toolName: string,
status: Exclude<ActionStatus, 'started'>,
hints: string[] | undefined,
session: SessionData,
requestId: string
): void {
// An empty array is still an API hints response, logged as hint_count 0.
if (process.env.CLOUD_SERVICE !== 'true' || !hints) return;
console.error(
'[MCP_AGENT_HINTS]',
JSON.stringify({
tool_name: toolName,
status,
request_id: requestId,
auth_type: session.authType ?? 'none',
// The companion search server shares this process and wrapper.
profile: session.profile ?? primaryProfile.id,
hint_count: hints.length,
hints: hints.map((hint) => hint.slice(0, AGENT_HINT_LOG_MAX_CHARS)),
})
);
}

function resultAgentHints(result: unknown): string[] | undefined {
return result && typeof result === 'object'
? readAgentHints((result as { structuredContent?: unknown }).structuredContent)
: undefined;
}

function guardHostedTool(
tool: RegisteredTool,
{ logActions }: { logActions: boolean }
Expand Down Expand Up @@ -1699,12 +1737,26 @@ function guardHostedTool(
if (logActions) emitActionLog(tool.name, 'error', invocationSession, new UserError(String(payload.message), payload), requestId, code);
throw new UserError(String(payload.message), payload);
}
const runTool = () =>
runWithCredentialRecovery(
() => execute(args, invocationContext),
requestId,
invocationSession
);
const runTool = async () => {
try {
const result = await runWithCredentialRecovery(
() => execute(args, invocationContext),
requestId,
invocationSession
);
emitAgentHintsLog(
tool.name,
(result as { isError?: boolean } | undefined)?.isError ? 'error' : 'success',
resultAgentHints(result),
invocationSession,
requestId
);
return result;
} catch (error) {
emitAgentHintsLog(tool.name, 'error', readErrorAgentHints(error), invocationSession, requestId);
throw error;
}
};
if (!logActions) return runTool();

emitActionLog(tool.name, 'started', invocationSession, undefined, requestId);
Expand Down
27 changes: 25 additions & 2 deletions tests/agent-hints.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async function startTransport(t, backendPort) {
try {
await waitForHealth(port, child);
t.after(() => stopChild(child));
return port;
return { port, getStderr: () => stderr };
} catch (error) {
await stopChild(child);
if (!stderr.includes('EADDRINUSE') || attempt === 3) {
Expand Down Expand Up @@ -227,7 +227,7 @@ test('MCP transport preserves hints on empty, readable, crawl and error results'
});
const backendPort = await listen(backend);
t.after(() => close(backend));
const port = await startTransport(t, backendPort);
const { port, getStderr } = await startTransport(t, backendPort);

const cases = [
{ name: 'firecrawl_search', arguments: { query: 'empty' } },
Expand Down Expand Up @@ -293,4 +293,27 @@ test('MCP transport preserves hints on empty, readable, crawl and error results'
for (const request of requests) {
assert.equal(request.headers['x-firecrawl-agent-hints'], 'true');
}

// stderr arrives on its own pipe and can trail the HTTP responses.
const hintLines = () =>
getStderr().split('\n').filter((line) => line.startsWith('[MCP_AGENT_HINTS] '));
for (let waited = 0; hintLines().length < cases.length && waited < 5_000; waited += 25) {
await new Promise((resolve) => setTimeout(resolve, 25));
}
const hintLogs = getStderr()
.split('\n')
.filter((line) => line.startsWith('[MCP_AGENT_HINTS] '))
.map((line) => JSON.parse(line.slice('[MCP_AGENT_HINTS] '.length)));
assert.equal(hintLogs.length, cases.length, 'one hint log per tool call');
for (const [index, log] of hintLogs.entries()) {
assert.equal(log.tool_name, cases[index].name);
assert.equal(log.hint_count, hints.length);
assert.deepEqual(log.hints, hints);
assert.equal(log.auth_type, 'api-key');
assert.match(log.request_id, /^[0-9a-f-]{36}$/);
}
assert.deepEqual(
hintLogs.map((log) => log.status),
['success', 'error', 'success', 'success', 'success', 'success', 'error', 'success', 'success', 'error']
);
});
Loading