feat(websearch): finish managed third-party rollout

- steer third-party launches toward the managed WebSearch MCP tool

- add opt-in trace diagnostics across launch, MCP, provider, and headless paths

- extend docs and regression coverage for the first-class runtime

Refs #862
This commit is contained in:
Tam Nhu Tran committed 2026-03-30 22:41:51 -04:00
1 parent b087c02738
commit de7171d810
21 files changed
+1281 -92

No files matched your search

+122 -8
View File
@@ -1,19 +1,35 @@
#!/usr/bin/env node
const {
getActiveProviderIds,
getQueryFingerprint,
getSkipReason,
hasAnyActiveProviders,
runLocalWebSearch,
shouldSkipHook,
traceWebSearchEvent,
} = require('../hooks/websearch-transformer.cjs');
const PROTOCOL_VERSION = '2024-11-05';
const SERVER_NAME = 'ccs-websearch';
const SERVER_VERSION = '1.0.0';
const TOOL_NAME = 'search';
const TOOL_NAME = 'WebSearch';
const TOOL_ALIASES = ['search'];
const TOOL_DESCRIPTION =
'Search the web through CCS-managed providers. Provider order: Exa, Tavily, Brave Search, DuckDuckGo, then optional legacy CLI fallback.';
'Third-party WebSearch replacement for CCS-managed Claude launches. Use this instead of Bash/curl/http fetches for web lookups. Provider order: Exa, Tavily, Brave Search, DuckDuckGo, then optional legacy CLI fallback.';
function isSupportedToolName(name) {
return name === TOOL_NAME || TOOL_ALIASES.includes(name);
}
let inputBuffer = Buffer.alloc(0);
const sessionState = {
initializeCount: 0,
toolsListCount: 0,
exposed: false,
toolCalls: 0,
};
let sessionSummaryWritten = false;
function shouldExposeTools() {
return !shouldSkipHook() && hasAnyActiveProviders();
@@ -33,7 +49,8 @@ function getTools() {
properties: {
query: {
type: 'string',
description: 'The search query to run against CCS WebSearch providers.',
description:
'Web query to resolve through CCS providers. Prefer this tool over ad hoc Bash/curl lookups when you need current web information.',
},
},
required: ['query'],
@@ -72,13 +89,36 @@ async function handleToolCall(message) {
const id = message.id;
const params = message.params || {};
const toolArgs = params.arguments || {};
const toolName = params.name || '<missing>';
const query = typeof toolArgs.query === 'string' ? toolArgs.query.trim() : '';
const fingerprint = getQueryFingerprint(query);
if (params.name !== TOOL_NAME) {
writeError(id, -32602, `Unknown tool: ${params.name || '<missing>'}`);
if (!isSupportedToolName(toolName)) {
traceWebSearchEvent('mcp_tool_call_rejected', {
source: 'mcp',
reason: 'unknown_tool',
toolName,
});
writeError(id, -32602, `Unknown tool: ${toolName}`);
return;
}
sessionState.toolCalls += 1;
traceWebSearchEvent('mcp_tool_call_received', {
source: 'mcp',
toolName,
...fingerprint,
});
if (!shouldExposeTools()) {
traceWebSearchEvent('mcp_tool_call_unavailable', {
source: 'mcp',
toolName,
exposed: false,
skipReason: getSkipReason(),
activeProviderIds: getActiveProviderIds(),
...fingerprint,
});
writeResponse(id, {
content: [
{
@@ -91,20 +131,41 @@ async function handleToolCall(message) {
return;
}
const query = typeof toolArgs.query === 'string' ? toolArgs.query.trim() : '';
if (!query) {
writeError(id, -32602, 'Tool "search" requires a non-empty string query.');
traceWebSearchEvent('mcp_tool_call_rejected', {
source: 'mcp',
reason: 'empty_query',
toolName,
});
writeError(id, -32602, `Tool "${TOOL_NAME}" requires a non-empty string query.`);
return;
}
const result = await runLocalWebSearch(query);
if (result.success) {
traceWebSearchEvent('mcp_tool_call_result', {
source: 'mcp',
toolName,
success: true,
providerId: result.providerId,
providerName: result.providerName,
...fingerprint,
});
writeResponse(id, {
content: [{ type: 'text', text: result.content }],
});
return;
}
traceWebSearchEvent('mcp_tool_call_result', {
source: 'mcp',
toolName,
success: false,
noActiveProviders: Boolean(result.noActiveProviders),
errorCount: result.errors.length,
...fingerprint,
});
const errorDetail =
result.noActiveProviders || result.errors.length === 0
? 'No active WebSearch providers are ready.'
@@ -128,6 +189,14 @@ async function handleMessage(message) {
switch (message.method) {
case 'initialize':
sessionState.initializeCount += 1;
sessionState.exposed = sessionState.exposed || shouldExposeTools();
traceWebSearchEvent('mcp_initialize', {
source: 'mcp',
exposed: shouldExposeTools(),
skipReason: getSkipReason(),
activeProviderIds: getActiveProviderIds(),
});
writeResponse(message.id, {
protocolVersion: PROTOCOL_VERSION,
capabilities: {
@@ -145,7 +214,20 @@ async function handleMessage(message) {
writeResponse(message.id, {});
return;
case 'tools/list':
writeResponse(message.id, { tools: getTools() });
sessionState.toolsListCount += 1;
{
const tools = getTools();
const exposed = tools.length > 0;
sessionState.exposed = sessionState.exposed || exposed;
traceWebSearchEvent('mcp_tools_list', {
source: 'mcp',
exposed,
toolNames: tools.map((tool) => tool.name),
activeProviderIds: getActiveProviderIds(),
skipReason: getSkipReason(),
});
writeResponse(message.id, { tools });
}
return;
case 'tools/call':
await handleToolCall(message);
@@ -157,6 +239,27 @@ async function handleMessage(message) {
}
}
function writeSessionSummary(exitCodeOrSignal) {
if (sessionSummaryWritten) {
return;
}
sessionSummaryWritten = true;
traceWebSearchEvent('mcp_session_summary', {
source: 'mcp',
initializeCount: sessionState.initializeCount,
toolsListCount: sessionState.toolsListCount,
exposed: sessionState.exposed,
toolCalls: sessionState.toolCalls,
calledWebSearch: sessionState.toolCalls > 0,
likelyBypassed: sessionState.exposed && sessionState.toolCalls === 0 ? 'unknown' : false,
activeProviderIds: getActiveProviderIds(),
skipReason: getSkipReason(),
exitCode: typeof exitCodeOrSignal === 'number' ? exitCodeOrSignal : null,
exitSignal: typeof exitCodeOrSignal === 'string' ? exitCodeOrSignal : null,
});
}
function parseMessages() {
while (true) {
const headerEnd = inputBuffer.indexOf('\r\n\r\n');
@@ -204,4 +307,15 @@ process.stdin.on('error', () => {
process.exit(0);
});
process.on('exit', (code) => {
writeSessionSummary(code);
});
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => {
process.on(signal, () => {
writeSessionSummary(signal);
process.exit(0);
});
});
process.stdin.resume();