Skip to content

Commit 42681eb

Browse files
author
Raymo
committed
feat: Add structured telemetry metrics, strict MCP config mode, and PermissionDecision types
- feat(telemetry): Add PermissionDecisionMetrics and ToolUsageMetrics types - feat(telemetry): Implement in-memory metrics store (per-session, non-persistent) - feat(telemetry): Export recordPermissionDecision, recordToolUsage, getSessionMetrics - feat(mcp-manager): Add strict MCP config mode with command allowlist validation - feat(mcp-manager): Support setStrictMode() for upstream --strict-mcp-config - feat(session): Wire strictMcpConfig from settings to McpManager - feat(settings): Add strictMcpConfig option to DeepcodingSettings schema - refactor: Re-export new telemetry types from core/index.ts Upstream reference: Claude Code --strict-mcp-config flag
1 parent edeb1b6 commit 42681eb

5 files changed

Lines changed: 132 additions & 1 deletion

File tree

packages/core/src/common/telemetry.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Upstream reference: claude_code_formatted.js L1867-L1884
2+
// OpenTelemetry-style structured metrics for observability
3+
14
const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new";
25
const DEFAULT_REPORT_TIMEOUT_MS = 3000;
36

@@ -7,6 +10,26 @@ export type NewPromptReportOptions = {
710
timeoutMs?: number;
811
};
912

13+
export type PermissionDecisionMetrics = {
14+
/** Tool type: "Edit" | "Write" | "Read" | "Bash" | "NotebookEdit" */
15+
toolType: string;
16+
/** Decision: "accept" | "reject" */
17+
decision: "accept" | "reject";
18+
};
19+
20+
export type ToolUsageMetrics = {
21+
/** Total cost in USD for the session */
22+
totalCostUSD: number;
23+
/** Total API duration in ms */
24+
totalAPIDuration: number;
25+
/** Lines of code added */
26+
totalLinesAdded: number;
27+
/** Lines of code removed */
28+
totalLinesRemoved: number;
29+
/** Number of tool calls made */
30+
totalToolCalls: number;
31+
};
32+
1033
/**
1134
* Fire-and-forget report of a new prompt session.
1235
* Respects the `enabled` toggle: when disabled, the call is a no-op.
@@ -32,3 +55,67 @@ export function reportNewPrompt(options: NewPromptReportOptions): void {
3255
.catch(() => {})
3356
.finally(() => clearTimeout(timeout));
3457
}
58+
59+
// In-memory metrics store (non-persistent, per-session)
60+
let sessionPermissionDecisions: { accept: number; reject: number } = { accept: 0, reject: 0 };
61+
let sessionToolUsage: ToolUsageMetrics = {
62+
totalCostUSD: 0,
63+
totalAPIDuration: 0,
64+
totalLinesAdded: 0,
65+
totalLinesRemoved: 0,
66+
totalToolCalls: 0,
67+
};
68+
69+
/**
70+
* Record a permission decision (accept/reject for a tool).
71+
* Upstream reference: code_edit_tool.decision counter
72+
*/
73+
export function recordPermissionDecision(decision: PermissionDecisionMetrics): void {
74+
if (decision.decision === "accept") {
75+
sessionPermissionDecisions.accept++;
76+
} else {
77+
sessionPermissionDecisions.reject++;
78+
}
79+
}
80+
81+
/**
82+
* Record tool usage metrics.
83+
* Upstream reference: totalToolDuration, totalCostUSD, totalLinesAdded/Removed
84+
*/
85+
export function recordToolUsage(metrics: Partial<ToolUsageMetrics>): void {
86+
if (metrics.totalCostUSD !== undefined) {
87+
sessionToolUsage.totalCostUSD += metrics.totalCostUSD;
88+
}
89+
if (metrics.totalAPIDuration !== undefined) {
90+
sessionToolUsage.totalAPIDuration += metrics.totalAPIDuration;
91+
}
92+
if (metrics.totalLinesAdded !== undefined) {
93+
sessionToolUsage.totalLinesAdded += metrics.totalLinesAdded;
94+
}
95+
if (metrics.totalLinesRemoved !== undefined) {
96+
sessionToolUsage.totalLinesRemoved += metrics.totalLinesRemoved;
97+
}
98+
if (metrics.totalToolCalls !== undefined) {
99+
sessionToolUsage.totalToolCalls += metrics.totalToolCalls;
100+
}
101+
}
102+
103+
export function getSessionPermissionDecisions(): { accept: number; reject: number } {
104+
return { ...sessionPermissionDecisions };
105+
}
106+
107+
export function getSessionToolUsage(): ToolUsageMetrics {
108+
return { ...sessionToolUsage };
109+
}
110+
111+
/** Reset all metrics (for testing or session restart) */
112+
export function resetSessionMetrics(): void {
113+
sessionPermissionDecisions = { accept: 0, reject: 0 };
114+
sessionToolUsage = {
115+
totalCostUSD: 0,
116+
totalAPIDuration: 0,
117+
totalLinesAdded: 0,
118+
totalLinesRemoved: 0,
119+
totalToolCalls: 0,
120+
};
121+
}

packages/core/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,16 @@ export { normalizeFilePath, getSnippet, clearSessionState, recordFileState, getF
100100
export { GitFileHistory } from "./common/file-history";
101101
export { killProcessTree } from "./common/process-tree";
102102
export { launchNotifyScript } from "./common/notify";
103-
export { reportNewPrompt } from "./common/telemetry";
103+
export {
104+
reportNewPrompt,
105+
recordPermissionDecision,
106+
recordToolUsage,
107+
getSessionPermissionDecisions,
108+
getSessionToolUsage,
109+
resetSessionMetrics,
110+
type PermissionDecisionMetrics,
111+
type ToolUsageMetrics,
112+
} from "./common/telemetry";
104113
export { DEEPSEEK_V4_MODELS, supportsMultimodal, defaultsToThinkingMode } from "./common/model-capabilities";
105114
export { findGitBashPath, resolveShellPath, setShellIfWindows } from "./common/shell-utils";
106115
export { logApiError } from "./common/error-logger";

packages/core/src/mcp/mcp-manager.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ const MCP_CALL_TOOL_TIMEOUT_MS = 60_000;
99
const API_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
1010
const API_TOOL_NAME_MAX_LENGTH = 64;
1111

12+
// Upstream reference: --strict-mcp-config
13+
// When strict mode is enabled, only these commands are allowed for MCP servers.
14+
const MCP_STRICT_ALLOWLIST_COMMANDS = new Set([
15+
"npx", "node", "python3", "python", "uvx", "uv",
16+
"bun", "deno", "go", "java",
17+
]);
18+
1219
type McpToolEntry = {
1320
serverName: string;
1421
originalName: string;
@@ -78,6 +85,12 @@ export class McpManager {
7885
private onToolsListChanged: (() => void) | null = null;
7986
private onStatusChanged: (() => void) | null = null;
8087
private serverConfigs: Record<string, McpServerConfig> = {};
88+
/** Upstream reference: --strict-mcp-config flag */
89+
private strictMode: boolean = false;
90+
91+
setStrictMode(enabled: boolean): void {
92+
this.strictMode = enabled;
93+
}
8194

8295
prepare(servers?: Record<string, McpServerConfig>): void {
8396
if (!servers || Object.keys(servers).length === 0) return;
@@ -146,6 +159,22 @@ export class McpManager {
146159
private async connectServer(name: string, config: McpServerConfig): Promise<void> {
147160
if (this.disposed) return;
148161

162+
// Strict mode: validate command against allowlist
163+
if (this.strictMode) {
164+
const commandName = config.command.split(/[\\/]/).pop() ?? config.command;
165+
if (!MCP_STRICT_ALLOWLIST_COMMANDS.has(commandName)) {
166+
const msg = `Strict MCP config: command "${config.command}" is not in the allowlist. ` +
167+
`Allowed commands: ${[...MCP_STRICT_ALLOWLIST_COMMANDS].join(", ")}. ` +
168+
`Disable strictMcpConfig in settings.json to bypass.`;
169+
this.setStatus({
170+
name, status: "failed", connected: false, error: msg,
171+
toolCount: 0, tools: [], promptCount: 0, prompts: [],
172+
resourceCount: 0, resources: [],
173+
});
174+
return;
175+
}
176+
}
177+
149178
// Clean up stale entries from previous connection attempts
150179
this.clients = this.clients.filter((c) => c.isConnected());
151180
this.tools = this.tools.filter((t) => t.serverName !== name);

packages/core/src/session.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ type SessionManagerOptions = {
304304
compressThreshold?: number;
305305
hooks?: HooksConfig;
306306
mcpServers?: Record<string, McpServerConfig>;
307+
strictMcpConfig: boolean;
307308
permissions?: Required<PermissionSettings>;
308309
enabledSkills?: Record<string, boolean>;
309310
};
@@ -333,6 +334,7 @@ export class SessionManager {
333334
compressThreshold?: number;
334335
hooks?: HooksConfig;
335336
mcpServers?: Record<string, McpServerConfig>;
337+
strictMcpConfig: boolean;
336338
permissions?: Required<PermissionSettings>;
337339
enabledSkills?: Record<string, boolean>;
338340
};
@@ -395,6 +397,7 @@ export class SessionManager {
395397
this.onProcessStdout = options.onProcessStdout;
396398
this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient, this.mcpManager);
397399
this.mcpManager.prepare(this.getResolvedSettings().mcpServers);
400+
this.mcpManager.setStrictMode(this.getResolvedSettings().strictMcpConfig);
398401
this.messageConverter = new OpenAIMessageConverter({
399402
renderInitPrompt: () => this.renderInitCommandPrompt(),
400403
});

packages/core/src/settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type DeepcodingSettings = {
104104
rulesDir?: string;
105105
compressThreshold?: number;
106106
mcpServers?: Record<string, McpServerConfig>;
107+
strictMcpConfig?: boolean;
107108
permissions?: PermissionSettings;
108109
enabledSkills?: EnabledSkillsSettings;
109110
statusline?: StatusLineSettings;
@@ -123,6 +124,7 @@ export type ResolvedDeepcodingSettings = {
123124
webSearchTool?: string;
124125
compressThreshold: number;
125126
mcpServers?: Record<string, McpServerConfig>;
127+
strictMcpConfig: boolean;
126128
permissions: Required<PermissionSettings>;
127129
enabledSkills: EnabledSkillsSettings;
128130
statusline: ResolvedStatusLineSettings;
@@ -572,6 +574,7 @@ export function resolveSettingsSources(
572574
webSearchTool: webSearchTool || undefined,
573575
compressThreshold,
574576
mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv),
577+
strictMcpConfig: projectSettings?.strictMcpConfig ?? userSettings?.strictMcpConfig ?? false,
575578
permissions: mergePermissions(userSettings, projectSettings),
576579
enabledSkills: mergeEnabledSkills(userSettings, projectSettings),
577580
statusline: mergeStatusLine(userSettings, projectSettings),

0 commit comments

Comments
 (0)