mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 02:15:36 +00:00
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:
1 parent
b087c02738
commit
de7171d810
21 files changed
+1281
-92
No files matched your search
@@ -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();
|
||||
Reference in new issue
Block a user