@debugmcp/mcp-debugger
Version:
Step-through debugging MCP server for LLMs
223 lines • 11.9 kB
JavaScript
/**
* Data retrieval operations for session management including variables,
* stack traces, and scopes.
*/
import { SessionState, DefaultAdapterPolicy, PythonAdapterPolicy, JsDebugAdapterPolicy, RustAdapterPolicy, MockAdapterPolicy, DebugLanguage } from '@debugmcp/shared';
import { SessionManagerCore } from './session-manager-core.js';
/**
* Data retrieval functionality for session management
*/
export class SessionManagerData extends SessionManagerCore {
/**
* Selects the appropriate adapter policy based on language
*/
selectPolicy(language) {
switch (language) {
case 'python':
case DebugLanguage.PYTHON:
return PythonAdapterPolicy;
case 'javascript':
case DebugLanguage.JAVASCRIPT:
return JsDebugAdapterPolicy;
case 'rust':
case DebugLanguage.RUST:
return RustAdapterPolicy;
case 'mock':
case DebugLanguage.MOCK:
return MockAdapterPolicy;
default:
return DefaultAdapterPolicy;
}
}
async getVariables(sessionId, variablesReference) {
const session = this._getSessionById(sessionId);
this.logger.info(`[SM getVariables ${sessionId}] Entered. variablesReference: ${variablesReference}, Current state: ${session.state}`);
if (!session.proxyManager || !session.proxyManager.isRunning()) {
this.logger.warn(`[SM getVariables ${sessionId}] No active proxy.`);
return [];
}
if (session.state !== SessionState.PAUSED) {
this.logger.warn(`[SM getVariables ${sessionId}] Session not paused. State: ${session.state}.`);
return [];
}
try {
this.logger.info(`[SM getVariables ${sessionId}] Sending DAP 'variables' for variablesReference ${variablesReference}.`);
const response = await session.proxyManager.sendDapRequest('variables', { variablesReference });
this.logger.info(`[SM getVariables ${sessionId}] DAP 'variables' response received. Body:`, response?.body);
if (response && response.body && response.body.variables) {
const vars = response.body.variables.map((v) => ({
name: v.name, value: v.value, type: v.type || "<unknown_type>",
variablesReference: v.variablesReference,
expandable: v.variablesReference > 0
}));
this.logger.info(`[SM getVariables ${sessionId}] Parsed variables:`, vars.map(v => ({ name: v.name, value: v.value, type: v.type })));
return vars;
}
this.logger.warn(`[SM getVariables ${sessionId}] No variables in response body for reference ${variablesReference}. Response:`, response);
return [];
}
catch (error) {
this.logger.error(`[SM getVariables ${sessionId}] Error getting variables:`, error);
return [];
}
}
async getStackTrace(sessionId, threadId, includeInternals = false) {
const session = this._getSessionById(sessionId);
const currentThreadId = session.proxyManager?.getCurrentThreadId();
this.logger.info(`[SM getStackTrace ${sessionId}] Entered. Requested threadId: ${threadId}, Current state: ${session.state}, Actual currentThreadId: ${currentThreadId}, includeInternals: ${includeInternals}`);
if (!session.proxyManager || !session.proxyManager.isRunning()) {
this.logger.warn(`[SM getStackTrace ${sessionId}] No active proxy.`);
return [];
}
if (session.state !== SessionState.PAUSED) {
this.logger.warn(`[SM getStackTrace ${sessionId}] Session not paused. State: ${session.state}.`);
return [];
}
const currentThreadForRequest = threadId || currentThreadId;
if (typeof currentThreadForRequest !== 'number') {
this.logger.warn(`[SM getStackTrace ${sessionId}] No effective thread ID to use.`);
return [];
}
try {
this.logger.info(`[SM getStackTrace ${sessionId}] Sending DAP 'stackTrace' for threadId ${currentThreadForRequest}.`);
const response = await session.proxyManager.sendDapRequest('stackTrace', { threadId: currentThreadForRequest });
this.logger.info(`[SM getStackTrace ${sessionId}] DAP 'stackTrace' response received. Body:`, response?.body);
if (response && response.body && response.body.stackFrames) {
let frames = response.body.stackFrames.map((sf) => ({
id: sf.id, name: sf.name,
file: sf.source?.path || sf.source?.name || "<unknown_source>",
line: sf.line, column: sf.column
}));
// Apply filtering using the language's policy
const policy = this.selectPolicy(session.language);
if (policy.filterStackFrames) {
this.logger.info(`[SM getStackTrace ${sessionId}] Applying stack frame filtering for ${session.language}. Original count: ${frames.length}`);
frames = policy.filterStackFrames(frames, includeInternals);
this.logger.info(`[SM getStackTrace ${sessionId}] After filtering: ${frames.length} frames`);
}
this.logger.info(`[SM getStackTrace ${sessionId}] Parsed stack frames (top 3):`, frames.slice(0, 3).map(f => ({ name: f.name, file: f.file, line: f.line })));
return frames;
}
this.logger.warn(`[SM getStackTrace ${sessionId}] No stackFrames in response body. Response:`, response);
return [];
}
catch (error) {
this.logger.error(`[SM getStackTrace ${sessionId}] Error getting stack trace:`, error);
return [];
}
}
async getScopes(sessionId, frameId) {
const session = this._getSessionById(sessionId);
this.logger.info(`[SM getScopes ${sessionId}] Entered. frameId: ${frameId}, Current state: ${session.state}`);
if (!session.proxyManager || !session.proxyManager.isRunning()) {
this.logger.warn(`[SM getScopes ${sessionId}] No active proxy.`);
return [];
}
if (session.state !== SessionState.PAUSED) {
this.logger.warn(`[SM getScopes ${sessionId}] Session not paused. State: ${session.state}.`);
return [];
}
try {
this.logger.info(`[SM getScopes ${sessionId}] Sending DAP 'scopes' for frameId ${frameId}.`);
const response = await session.proxyManager.sendDapRequest('scopes', { frameId });
this.logger.info(`[SM getScopes ${sessionId}] DAP 'scopes' response received. Body:`, response?.body);
if (response && response.body && response.body.scopes) {
this.logger.info(`[SM getScopes ${sessionId}] Parsed scopes:`, response.body.scopes.map(s => ({ name: s.name, ref: s.variablesReference, expensive: s.expensive })));
return response.body.scopes;
}
this.logger.warn(`[GetScopes] No scopes in response body for session ${sessionId}, frameId ${frameId}. Response:`, response);
return [];
}
catch (error) {
this.logger.error(`[SM getScopes ${sessionId}] Error getting scopes:`, error);
return [];
}
}
/**
* Get local variables for the current or specified stack frame.
* This is a convenience method that orchestrates getting stack trace,
* scopes, and variables, then delegates to the adapter policy to extract
* just the local variables.
*/
async getLocalVariables(sessionId, includeSpecial = false) {
const session = this._getSessionById(sessionId);
this.logger.info(`[SM getLocalVariables ${sessionId}] Entered. includeSpecial: ${includeSpecial}, Current state: ${session.state}`);
// Validate session state
if (!session.proxyManager || !session.proxyManager.isRunning()) {
this.logger.warn(`[SM getLocalVariables ${sessionId}] No active proxy.`);
return { variables: [], frame: null, scopeName: null };
}
if (session.state !== SessionState.PAUSED) {
this.logger.warn(`[SM getLocalVariables ${sessionId}] Session not paused. State: ${session.state}.`);
return { variables: [], frame: null, scopeName: null };
}
try {
// Step 1: Get stack trace
const stackFrames = await this.getStackTrace(sessionId);
if (!stackFrames || stackFrames.length === 0) {
this.logger.warn(`[SM getLocalVariables ${sessionId}] No stack frames available.`);
return { variables: [], frame: null, scopeName: null };
}
const topFrame = stackFrames[0];
this.logger.info(`[SM getLocalVariables ${sessionId}] Top frame: ${topFrame.name} at ${topFrame.file}:${topFrame.line}`);
// Step 2: Collect all scopes for all frames (may need multiple frames for closures)
const scopesMap = {};
for (const frame of stackFrames) {
const scopes = await this.getScopes(sessionId, frame.id);
if (scopes && scopes.length > 0) {
scopesMap[frame.id] = scopes;
}
}
// Step 3: Collect variables for all scopes
const variablesMap = {};
for (const frameId in scopesMap) {
const scopes = scopesMap[frameId];
for (const scope of scopes) {
if (scope.variablesReference > 0) {
const variables = await this.getVariables(sessionId, scope.variablesReference);
if (variables && variables.length > 0) {
variablesMap[scope.variablesReference] = variables;
}
}
}
}
// Step 4: Get the appropriate adapter policy
const policy = this.selectPolicy(session.language);
// Step 5: Extract local variables using the adapter policy
let localVars = [];
let scopeName = null;
if (policy.extractLocalVariables) {
localVars = policy.extractLocalVariables(stackFrames, scopesMap, variablesMap, includeSpecial);
// Get the scope name for reporting
if (policy.getLocalScopeName) {
const scopeNames = policy.getLocalScopeName();
scopeName = Array.isArray(scopeNames) ? scopeNames[0] : scopeNames;
}
}
else {
// Fallback: use first non-global scope from top frame
const topFrameScopes = scopesMap[topFrame.id] || [];
const localScope = topFrameScopes.find(s => !s.name.toLowerCase().includes('global'));
if (localScope) {
localVars = variablesMap[localScope.variablesReference] || [];
scopeName = localScope.name;
}
}
this.logger.info(`[SM getLocalVariables ${sessionId}] Found ${localVars.length} local variables.`);
return {
variables: localVars,
frame: {
name: topFrame.name,
file: topFrame.file,
line: topFrame.line
},
scopeName
};
}
catch (error) {
this.logger.error(`[SM getLocalVariables ${sessionId}] Error getting local variables:`, error);
return { variables: [], frame: null, scopeName: null };
}
}
}
//# sourceMappingURL=session-manager-data.js.map