Skip to content
Merged
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
42 changes: 42 additions & 0 deletions packages/mcp/src/sf-mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ export class SfMcpServer extends McpServer implements ToolMethodSignatures {
this.logger.debug(`Tool ${name} completed in ${runtimeMs}ms`);
if (result.isError) this.logger.debug(`Tool ${name} errored`);

// Calculate response character count for token usage (never let telemetry instrumentation fail a tool call)
let responseCharCount = 0;
try {
responseCharCount = this.calculateResponseCharCount(result);
} catch (err) {
// never let telemetry instrumentation fail a tool call
}

this.telemetry?.sendEvent('TOOL_CALLED', {
name,
runtimeMs,
Expand All @@ -151,6 +159,7 @@ export class SfMcpServer extends McpServer implements ToolMethodSignatures {
//
// https://modelcontextprotocol.io/specification/2025-06-18/schema#calltoolresult
isError: result.isError ?? false,
responseCharCount: responseCharCount.toString(),
});

this.telemetry?.sendPdpEvent({
Expand All @@ -165,4 +174,37 @@ export class SfMcpServer extends McpServer implements ToolMethodSignatures {
const tool = super.registerTool(name, config, wrappedCb as ToolCallback<InputArgs>);
return tool;
}

/**
* Calculates the total character count from tool result content and structured output.
* Used for token usage. Accounts for both:
* - content: text (and other) content items
* - structuredContent: structured tool output when the tool defines an outputSchema
*
* @see https://modelcontextprotocol.io/specification/2025-11-25/server/tools#output-schema
* @param result - The CallToolResult from tool execution
* @returns Total character count across text content and structured content
*/
private calculateResponseCharCount(result: CallToolResult): number {
let total = 0;

// Plain text (and other) content items
if (result.content && Array.isArray(result.content)) {
total += result.content
.filter((item): item is { type: 'text'; text: string } => item.type === 'text')
.reduce((sum, item) => sum + item.text.length, 0);
}

// Structured content (JSON object per outputSchema)
const structured = (result as CallToolResult & { structuredContent?: unknown }).structuredContent;
if (structured !== undefined && structured !== null && typeof structured === 'object') {
try {
total += JSON.stringify(structured).length;
} catch {
// ignore serialization errors
}
}

return total;
}
}
Loading