project_context.ts•26.9 kB
// Project Context Tools for Agent-MCP Node.js
// Ported from Python project_context_tools.py to match API exactly
import { z } from 'zod';
import { registerTool } from './registry.js';
import { getDbConnection } from '../db/connection.js';
import { MCP_DEBUG } from '../core/config.js';
import { verifyToken, getAgentId } from '../core/auth.js';
// Schemas matching Python implementation
const ViewProjectContextSchema = z.object({
token: z.string().describe("Authentication token"),
context_key: z.string().optional().describe("Exact key to view (optional). If provided, search_query is ignored."),
search_query: z.string().optional().describe("Keyword search query (optional). Searches keys, descriptions, and values."),
show_health_analysis: z.boolean().optional().default(false).describe("Include comprehensive health metrics and analysis"),
show_stale_entries: z.boolean().optional().default(false).describe("Show only entries older than 30 days needing review"),
include_backup_info: z.boolean().optional().default(false).describe("Include backup recommendations and info"),
max_results: z.number().int().min(1).max(200).optional().default(50).describe("Maximum number of entries to return"),
sort_by: z.enum(['key', 'last_updated', 'size']).optional().default('last_updated').describe("Sort entries by specified field")
});
const UpdateProjectContextSchema = z.object({
token: z.string().describe("Authentication token"),
context_key: z.string().describe("Key for the context entry"),
context_value: z.any().describe("Value to store (will be JSON stringified)"),
description: z.string().optional().describe("Optional description of what this context represents")
});
const BulkUpdateProjectContextSchema = z.object({
token: z.string().describe("Authentication token"),
updates: z.array(z.object({
context_key: z.string(),
context_value: z.any(),
description: z.string().optional()
})).describe("Array of context updates to perform")
});
const DeleteProjectContextSchema = z.object({
token: z.string().describe("Authentication token"),
context_key: z.string().describe("Context key to delete")
});
const BackupProjectContextSchema = z.object({
token: z.string().describe("Authentication token"),
backup_name: z.string().optional().describe("Custom name for the backup")
});
const ValidateContextConsistencySchema = z.object({
token: z.string().describe("Authentication token"),
fix_issues: z.boolean().optional().default(false).describe("Automatically fix found issues")
});
/**
* Analyze context health - ported from Python _analyze_context_health
*/
function analyzeContextHealth(contextEntries: any[]) {
if (!contextEntries || contextEntries.length === 0) {
return { status: "no_data", total: 0 };
}
const total = contextEntries.length;
const issues: string[] = [];
const warnings: string[] = [];
let staleCount = 0;
let jsonErrors = 0;
let largeEntries = 0;
const currentTime = new Date();
for (const entry of contextEntries) {
const contextKey = entry.context_key || "unknown";
const value = entry.value || "";
const lastUpdated = entry.last_updated;
// Check for JSON parsing issues
try {
if (typeof value === 'string') {
JSON.parse(value);
}
} catch {
jsonErrors += 1;
issues.push(`JSON parse error in '${contextKey}'`);
}
// Check for stale entries (30+ days old)
if (lastUpdated) {
try {
const updatedTime = new Date(lastUpdated);
const daysOld = Math.floor((currentTime.getTime() - updatedTime.getTime()) / (1000 * 60 * 60 * 24));
if (daysOld > 30) {
staleCount += 1;
if (daysOld > 90) {
warnings.push(`'${contextKey}' is ${daysOld} days old`);
}
}
} catch {
warnings.push(`Invalid timestamp for '${contextKey}'`);
}
}
// Check for oversized entries (>10KB)
const entrySize = String(value).length;
if (entrySize > 10240) { // 10KB
largeEntries += 1;
warnings.push(`'${contextKey}' is large (${Math.floor(entrySize/1024)}KB)`);
}
}
// Calculate health score
const staleRatio = staleCount / total;
const errorRatio = jsonErrors / total;
const largeRatio = largeEntries / total;
const healthScore = Math.max(
0, Math.min(100, 100 - (staleRatio * 40) - (errorRatio * 50) - (largeRatio * 10))
);
const healthStatus = healthScore >= 90 ? "excellent" :
healthScore >= 70 ? "good" :
healthScore >= 50 ? "needs_attention" : "critical";
return {
status: healthStatus,
health_score: Math.round(healthScore * 10) / 10,
total,
stale_entries: staleCount,
json_errors: jsonErrors,
large_entries: largeEntries,
issues: issues.slice(0, 5), // Limit to first 5
warnings: warnings.slice(0, 5), // Limit to first 5
recommendations: generateContextRecommendations(staleCount, jsonErrors, largeEntries, total)
};
}
/**
* Generate context recommendations - ported from Python
*/
function generateContextRecommendations(staleCount: number, jsonErrors: number, largeEntries: number, total: number): string[] {
const recommendations: string[] = [];
if (staleCount > 0) {
recommendations.push(`Review ${staleCount} stale entries that haven't been updated in 30+ days`);
}
if (jsonErrors > 0) {
recommendations.push(`Fix ${jsonErrors} entries with JSON parsing errors`);
}
if (largeEntries > 0) {
recommendations.push(`Consider splitting ${largeEntries} large entries (>10KB) into smaller pieces`);
}
if (total > 100) {
recommendations.push("Consider creating context backups due to large number of entries");
}
return recommendations;
}
/**
* View project context - ported from Python view_project_context_tool_impl
*/
async function viewProjectContext(args: Record<string, any>) {
const {
token,
context_key,
search_query,
show_health_analysis = false,
show_stale_entries = false,
include_backup_info = false,
max_results = 50,
sort_by = 'last_updated'
} = args;
// Authenticate
const agentId = getAgentId(token);
if (!agentId) {
return {
content: [{
type: 'text' as const,
text: "Unauthorized: Valid token required"
}],
isError: true
};
}
const db = getDbConnection();
try {
// Build query based on filters
const whereConditions: string[] = [];
const queryParams: any[] = [];
if (context_key) {
whereConditions.push("context_key = ?");
queryParams.push(context_key);
} else if (search_query) {
const likePattern = `%${search_query}%`;
whereConditions.push("(context_key LIKE ? OR description LIKE ? OR value LIKE ?)");
queryParams.push(likePattern, likePattern, likePattern);
}
if (show_stale_entries) {
// Show entries older than 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
whereConditions.push("last_updated < ?");
queryParams.push(thirtyDaysAgo);
}
// Build query with smart sorting
let baseQuery = "SELECT context_key, value, description, updated_by, last_updated, LENGTH(value) as value_size FROM project_context";
if (whereConditions.length > 0) {
baseQuery += " WHERE " + whereConditions.join(" AND ");
}
// Smart sorting
if (sort_by === "size") {
baseQuery += " ORDER BY LENGTH(value) DESC";
} else if (sort_by === "key") {
baseQuery += " ORDER BY context_key ASC";
} else { // last_updated (default)
baseQuery += " ORDER BY last_updated DESC";
}
baseQuery += ` LIMIT ${max_results}`;
const stmt = db.prepare(baseQuery);
const rows = stmt.all(...queryParams);
if (rows.length === 0) {
const message = context_key ?
`Context key '${context_key}' not found` :
'No project context entries found';
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ message, total_entries: 0 }, null, 2)
}]
};
}
// Process results with enhanced information
const processedResults = [];
for (const row of rows as any[]) {
try {
let valueParsed;
let jsonValid = true;
try {
valueParsed = JSON.parse(row.value);
} catch {
valueParsed = row.value;
jsonValid = false;
}
// Calculate additional metadata
const entrySize = String(row.value).length;
const lastUpdated = row.last_updated;
let daysOld = null;
if (lastUpdated) {
try {
const updatedTime = new Date(lastUpdated);
daysOld = Math.floor((Date.now() - updatedTime.getTime()) / (1000 * 60 * 60 * 24));
} catch {
// Invalid date
}
}
processedResults.push({
context_key: row.context_key,
value: valueParsed,
description: row.description,
updated_by: row.updated_by,
last_updated: lastUpdated,
metadata: {
size_bytes: entrySize,
size_kb: Math.round(entrySize / 1024 * 10) / 10,
json_valid: jsonValid,
days_old: daysOld,
is_stale: daysOld !== null && daysOld > 30
}
});
} catch (error) {
// Skip problematic entries but log
if (MCP_DEBUG) {
console.error(`Error processing context entry ${row.context_key}:`, error);
}
}
}
// Build response
const response: any = {
total_entries: processedResults.length,
max_results_limit: max_results,
sort_by,
entries: processedResults
};
// Add health analysis if requested
if (show_health_analysis) {
response.health_analysis = analyzeContextHealth(rows);
}
// Add backup info if requested
if (include_backup_info) {
// Check for existing backups
const backupStmt = db.prepare("SELECT context_key, last_updated FROM project_context WHERE context_key LIKE '__backup__%' ORDER BY last_updated DESC LIMIT 5");
const backups = backupStmt.all();
response.backup_info = {
recent_backups: backups,
recommendation: backups.length === 0 ?
"No backups found - consider creating one" :
`${backups.length} backups available`
};
}
// Log action
const actionStmt = db.prepare(`
INSERT INTO agent_actions (agent_id, action_type, timestamp, details)
VALUES (?, ?, ?, ?)
`);
actionStmt.run(agentId, 'view_project_context', new Date().toISOString(), JSON.stringify({
context_key,
search_query,
results_count: processedResults.length
}));
return {
content: [{
type: 'text' as const,
text: JSON.stringify(response, null, 2)
}]
};
} catch (error) {
console.error('Error viewing project context:', error);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Failed to view project context',
details: error instanceof Error ? error.message : String(error)
}, null, 2)
}],
isError: true
};
}
}
/**
* Update project context - ported from Python update_project_context_tool_impl
*/
async function updateProjectContext(args: Record<string, any>) {
const { token, context_key, context_value, description } = args;
// Authenticate
const agentId = getAgentId(token);
if (!agentId) {
return {
content: [{
type: 'text' as const,
text: "Unauthorized: Valid token required"
}],
isError: true
};
}
const db = getDbConnection();
try {
// Ensure value is JSON serializable
let valueJsonStr: string;
try {
valueJsonStr = JSON.stringify(context_value);
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `Error: Provided context_value is not JSON serializable: ${error}`
}],
isError: true
};
}
const timestamp = new Date().toISOString();
// Use INSERT OR REPLACE (UPSERT)
const upsertStmt = db.prepare(`
INSERT OR REPLACE INTO project_context (context_key, value, last_updated, updated_by, description)
VALUES (?, ?, ?, ?, ?)
`);
upsertStmt.run(context_key, valueJsonStr, timestamp, agentId, description || null);
// Log action
const actionStmt = db.prepare(`
INSERT INTO agent_actions (agent_id, action_type, timestamp, details)
VALUES (?, ?, ?, ?)
`);
actionStmt.run(agentId, 'updated_context', timestamp, JSON.stringify({
context_key,
action: 'set/update'
}));
if (MCP_DEBUG) {
console.log(`📝 Project context '${context_key}' updated by '${agentId}'`);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
context_key,
updated_by: agentId,
timestamp,
value_size_bytes: valueJsonStr.length,
description
}, null, 2)
}]
};
} catch (error) {
console.error('Error updating project context:', error);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Failed to update project context',
details: error instanceof Error ? error.message : String(error)
}, null, 2)
}],
isError: true
};
}
}
/**
* Bulk update project context - ported from Python bulk_update_project_context_tool_impl
*/
async function bulkUpdateProjectContext(args: Record<string, any>) {
const { token, updates } = args;
// Authenticate
const agentId = getAgentId(token);
if (!agentId) {
return {
content: [{
type: 'text' as const,
text: "Unauthorized: Valid token required"
}],
isError: true
};
}
const db = getDbConnection();
try {
const timestamp = new Date().toISOString();
const results: any[] = [];
// Use transaction for bulk update
const transaction = db.transaction(() => {
for (const update of updates) {
const { context_key, context_value, description } = update;
// Ensure value is JSON serializable
let valueJsonStr: string;
try {
valueJsonStr = JSON.stringify(context_value);
} catch (error) {
results.push({
context_key,
success: false,
error: `Value not JSON serializable: ${error}`
});
continue;
}
// Insert or update
const upsertStmt = db.prepare(`
INSERT OR REPLACE INTO project_context (context_key, value, last_updated, updated_by, description)
VALUES (?, ?, ?, ?, ?)
`);
upsertStmt.run(context_key, valueJsonStr, timestamp, agentId, description || null);
results.push({
context_key,
success: true,
value_size_bytes: valueJsonStr.length
});
}
// Log bulk action
const actionStmt = db.prepare(`
INSERT INTO agent_actions (agent_id, action_type, timestamp, details)
VALUES (?, ?, ?, ?)
`);
actionStmt.run(agentId, 'bulk_update_context', timestamp, JSON.stringify({
updates_count: updates.length
}));
});
transaction();
if (MCP_DEBUG) {
console.log(`📝 Bulk context update: ${updates.length} entries by ${agentId}`);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
updates_processed: results.length,
updated_by: agentId,
timestamp,
results
}, null, 2)
}]
};
} catch (error) {
console.error('Error bulk updating project context:', error);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Failed to bulk update project context',
details: error instanceof Error ? error.message : String(error)
}, null, 2)
}],
isError: true
};
}
}
/**
* Delete project context - ported from Python delete_project_context_tool_impl
*/
async function deleteProjectContext(args: Record<string, any>) {
const { token, context_key } = args;
// Authenticate
const agentId = getAgentId(token);
if (!agentId) {
return {
content: [{
type: 'text' as const,
text: "Unauthorized: Valid token required"
}],
isError: true
};
}
const db = getDbConnection();
try {
// Check if entry exists
const existingStmt = db.prepare('SELECT context_key, value FROM project_context WHERE context_key = ?');
const existing = existingStmt.get(context_key);
if (!existing) {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: false,
error: `Context key '${context_key}' not found`
}, null, 2)
}],
isError: true
};
}
// Delete the entry
const deleteStmt = db.prepare('DELETE FROM project_context WHERE context_key = ?');
const result = deleteStmt.run(context_key);
// Log action
const timestamp = new Date().toISOString();
const actionStmt = db.prepare(`
INSERT INTO agent_actions (agent_id, action_type, timestamp, details)
VALUES (?, ?, ?, ?)
`);
actionStmt.run(agentId, 'delete_context', timestamp, JSON.stringify({
context_key,
deleted_value_size: String((existing as any).value).length
}));
if (MCP_DEBUG) {
console.log(`🗑️ Context deleted: ${context_key} by ${agentId}`);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
context_key,
deleted_by: agentId,
timestamp,
deleted: result.changes > 0
}, null, 2)
}]
};
} catch (error) {
console.error('Error deleting project context:', error);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Failed to delete project context',
details: error instanceof Error ? error.message : String(error)
}, null, 2)
}],
isError: true
};
}
}
/**
* Create backup of project context - ported from Python backup_project_context_tool_impl
*/
async function backupProjectContext(args: Record<string, any>) {
const { token, backup_name } = args;
// Authenticate
const agentId = getAgentId(token);
if (!agentId) {
return {
content: [{
type: 'text' as const,
text: "Unauthorized: Valid token required"
}],
isError: true
};
}
const db = getDbConnection();
try {
const timestamp = new Date().toISOString();
const backupId = backup_name || `backup_${timestamp.replace(/[:.]/g, '-')}`;
// Get all context entries
const stmt = db.prepare('SELECT * FROM project_context WHERE context_key NOT LIKE "__backup__%" ORDER BY context_key');
const entries = stmt.all();
const backup = {
backup_id: backupId,
created_at: timestamp,
created_by: agentId,
entry_count: entries.length,
entries: entries.map((entry: any) => ({
context_key: entry.context_key,
value: JSON.parse(entry.value),
description: entry.description,
last_updated: entry.last_updated,
updated_by: entry.updated_by
}))
};
// Store backup as a special context entry
const backupKey = `__backup__${backupId}`;
const backupJson = JSON.stringify(backup);
const insertStmt = db.prepare(`
INSERT INTO project_context (context_key, value, last_updated, updated_by, description)
VALUES (?, ?, ?, ?, ?)
`);
insertStmt.run(backupKey, backupJson, timestamp, agentId, `Backup created by ${agentId}`);
// Log action
const actionStmt = db.prepare(`
INSERT INTO agent_actions (agent_id, action_type, timestamp, details)
VALUES (?, ?, ?, ?)
`);
actionStmt.run(agentId, 'backup_context', timestamp, JSON.stringify({
backup_id: backupId,
entry_count: entries.length
}));
if (MCP_DEBUG) {
console.log(`💾 Context backup created: ${backupId} by ${agentId}`);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
backup_id: backupId,
backup_key: backupKey,
entry_count: entries.length,
backup_size_bytes: backupJson.length,
created_by: agentId,
timestamp
}, null, 2)
}]
};
} catch (error) {
console.error('Error creating context backup:', error);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Failed to create context backup',
details: error instanceof Error ? error.message : String(error)
}, null, 2)
}],
isError: true
};
}
}
/**
* Validate context consistency - ported from Python validate_context_consistency_tool_impl
*/
async function validateContextConsistency(args: Record<string, any>) {
const { token, fix_issues = false } = args;
// Authenticate
const agentId = getAgentId(token);
if (!agentId) {
return {
content: [{
type: 'text' as const,
text: "Unauthorized: Valid token required"
}],
isError: true
};
}
const db = getDbConnection();
try {
const issues: any[] = [];
const fixes: any[] = [];
// Get all context entries
const stmt = db.prepare('SELECT * FROM project_context ORDER BY context_key');
const entries = stmt.all();
// Check for JSON parsing issues
for (const entry of entries as any[]) {
try {
JSON.parse(entry.value);
} catch {
issues.push({
type: 'invalid_json',
context_key: entry.context_key,
error: 'Value is not valid JSON'
});
if (fix_issues) {
// Try to fix by re-stringifying
try {
const fixedValue = JSON.stringify(entry.value);
const updateStmt = db.prepare('UPDATE project_context SET value = ? WHERE context_key = ?');
updateStmt.run(fixedValue, entry.context_key);
fixes.push({
type: 'fixed_json',
context_key: entry.context_key,
action: 'Re-stringified value'
});
} catch {
issues.push({
type: 'unfixable_json',
context_key: entry.context_key,
error: 'Could not fix JSON'
});
}
}
}
}
// Check for orphaned references (agents that no longer exist)
const agentStmt = db.prepare('SELECT DISTINCT updated_by FROM project_context');
const contextAgents = agentStmt.all() as { updated_by: string }[];
for (const contextAgent of contextAgents) {
if (contextAgent.updated_by === 'admin' || contextAgent.updated_by === 'server_startup') {
continue; // Skip system entries
}
const checkAgentStmt = db.prepare('SELECT agent_id FROM agents WHERE agent_id = ?');
const agentExists = checkAgentStmt.get(contextAgent.updated_by);
if (!agentExists) {
issues.push({
type: 'orphaned_agent_reference',
agent_id: contextAgent.updated_by,
error: 'Context references non-existent agent'
});
}
}
// Analyze context health
const health = analyzeContextHealth(entries);
// Log validation
const timestamp = new Date().toISOString();
const actionStmt = db.prepare(`
INSERT INTO agent_actions (agent_id, action_type, timestamp, details)
VALUES (?, ?, ?, ?)
`);
actionStmt.run(agentId, 'validate_context', timestamp, JSON.stringify({
issues_found: issues.length,
fixes_applied: fixes.length,
fix_issues
}));
if (MCP_DEBUG) {
console.log(`🔍 Context validation: ${issues.length} issues, ${fixes.length} fixes by ${agentId}`);
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
validation_summary: {
total_entries: entries.length,
issues_found: issues.length,
fixes_applied: fixes.length,
validation_date: timestamp,
validated_by: agentId
},
context_health: health,
issues,
fixes,
recommendations: health.recommendations
}, null, 2)
}]
};
} catch (error) {
console.error('Error validating context consistency:', error);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
error: 'Failed to validate context consistency',
details: error instanceof Error ? error.message : String(error)
}, null, 2)
}],
isError: true
};
}
}
// Register all project context tools
registerTool(
'view_project_context',
'Smart project context viewer with health analysis, stale entry detection, and advanced filtering. Provides comprehensive insights into context quality and usage.',
ViewProjectContextSchema,
viewProjectContext
);
registerTool(
'update_project_context',
'Store project context information that all agents can access. Context is stored as JSON and can be retrieved by any authenticated agent.',
UpdateProjectContextSchema,
updateProjectContext
);
registerTool(
'bulk_update_project_context',
'Update multiple project context entries in a single transaction for efficiency and consistency.',
BulkUpdateProjectContextSchema,
bulkUpdateProjectContext
);
registerTool(
'delete_project_context',
'Delete a specific project context entry. This action cannot be undone, so use with caution.',
DeleteProjectContextSchema,
deleteProjectContext
);
registerTool(
'backup_project_context',
'Create a backup snapshot of all project context entries for disaster recovery and version control.',
BackupProjectContextSchema,
backupProjectContext
);
registerTool(
'validate_context_consistency',
'Validate project context integrity, check for issues like invalid JSON or orphaned references, and optionally fix problems automatically.',
ValidateContextConsistencySchema,
validateContextConsistency
);
if (MCP_DEBUG) {
console.log('✅ Project context tools registered');
}
export {
viewProjectContext,
updateProjectContext,
bulkUpdateProjectContext,
deleteProjectContext,
backupProjectContext,
validateContextConsistency
};