UNPKG

@agentics.org/sparc2

Version:

SPARC 2.0 - Autonomous Vector Coding Agent + MCP. SPARC 2.0, vectorized AI code analysis, is an intelligent coding agent framework built to automate and streamline software development. It combines secure execution environments, and version control into

834 lines (774 loc) 25.1 kB
/** * MCP Server module for SPARC 2.0 * Implements a Model Context Protocol server for SPARC2 */ import { serve } from "https://deno.land/std@0.215.0/http/server.ts"; import { FileToProcess, SPARC2Agent } from "../agent/agent.ts"; import { logDebug, logError, logInfo } from "../logger.ts"; import { executeCode } from "../sandbox/codeInterpreter.ts"; import { createCheckpoint } from "../git/gitIntegration.ts"; // Default port for the MCP server const DEFAULT_PORT = 3001; /** * MCP Tool definition */ interface MCPTool { name: string; description: string; parameters: { type: string; properties: Record<string, { type: string; description: string; }>; required?: string[]; }; returns?: { type: string; description: string; }; } /** * MCP Resource definition */ interface MCPResource { name: string; type: string; description: string; properties: Record<string, { type: string; description: string; }>; methods?: Array<{ name: string; description: string; parameters: { type: string; properties: Record<string, { type: string; description: string; }>; required?: string[]; }; returns?: { type: string; description: string; }; }>; } /** * Start the MCP server * @param options Server options * @returns Promise that resolves when the server is started */ export async function startMCPServer(options: { port?: number; model?: string; mode?: "automatic" | "semi" | "manual" | "custom" | "interactive"; diffMode?: "file" | "function"; processing?: "sequential" | "parallel" | "concurrent" | "swarm"; configPath?: string; }): Promise<void> { const port = options.port || DEFAULT_PORT; // Initialize SPARC2 agent const agent = new SPARC2Agent({ model: options.model || "gpt-4o", mode: options.mode || "automatic", diffMode: options.diffMode || "file", processing: options.processing || "sequential", configPath: "./config/agent-config.toml", }); // Initialize the agent await agent.init(); await logInfo("Starting SPARC2 MCP server on port " + port); // Define available tools const tools: MCPTool[] = [ { name: "analyze_code", description: "Analyze code files for issues and improvements", parameters: { type: "object", properties: { files: { type: "array", description: "Array of file paths to analyze", }, task: { type: "string", description: "Description of the analysis task", }, }, required: ["files"], }, returns: { type: "object", description: "Analysis results with suggestions for improvements", }, }, { name: "modify_code", description: "Apply suggested modifications to code files", parameters: { type: "object", properties: { files: { type: "array", description: "Array of file paths to modify", }, task: { type: "string", description: "Description of the modification task", }, }, required: ["files"], }, returns: { type: "object", description: "Results of the modifications applied", }, }, { name: "execute_code", description: "Execute code in a secure sandbox", parameters: { type: "object", properties: { code: { type: "string", description: "Code to execute", }, language: { type: "string", description: "Programming language (python, javascript, typescript)", }, }, required: ["code", "language"], }, returns: { type: "object", description: "Execution results including stdout, stderr, and any errors", }, }, { name: "search_code", description: "Search for similar code changes", parameters: { type: "object", properties: { query: { type: "string", description: "Search query", }, limit: { type: "number", description: "Maximum number of results to return", }, }, required: ["query"], }, returns: { type: "array", description: "Array of search results with relevance scores", }, }, { name: "create_checkpoint", description: "Create a version control checkpoint", parameters: { type: "object", properties: { message: { type: "string", description: "Checkpoint message", }, }, required: ["message"], }, returns: { type: "object", description: "Checkpoint information including commit hash", }, }, { name: "rollback", description: "Roll back to a previous checkpoint", parameters: { type: "object", properties: { commit: { type: "string", description: "Commit hash to roll back to", }, }, required: ["commit"], }, returns: { type: "object", description: "Result of the rollback operation", }, }, { name: "config", description: "Manage configuration", parameters: { type: "object", properties: { action: { type: "string", description: "Action to perform (get, set, list)", }, key: { type: "string", description: "Configuration key", }, value: { type: "string", description: "Configuration value (for set action)", }, }, required: ["action"], }, returns: { type: "object", description: "Configuration operation result", }, }, ]; // Define available resources const resources: MCPResource[] = [ { name: "git_repository", type: "version_control", description: "Git repository for version control and checkpointing", properties: { path: { type: "string", description: "Path to the git repository", }, branch: { type: "string", description: "Current branch name", }, }, methods: [ { name: "create_checkpoint", description: "Create a checkpoint in the git repository", parameters: { type: "object", properties: { name: { type: "string", description: "Name of the checkpoint", }, }, required: ["name"], }, returns: { type: "object", description: "Checkpoint information including commit hash", }, }, { name: "rollback", description: "Roll back to a previous checkpoint", parameters: { type: "object", properties: { commit: { type: "string", description: "Commit hash to roll back to", }, }, required: ["commit"], }, returns: { type: "object", description: "Result of the rollback operation", }, }, ], }, { name: "vector_store", type: "database", description: "Vector database for storing and searching code changes and logs", properties: { id: { type: "string", description: "ID of the vector store", }, size: { type: "number", description: "Number of entries in the vector store", }, }, methods: [ { name: "search", description: "Search for similar entries in the vector store", parameters: { type: "object", properties: { query: { type: "string", description: "Search query", }, limit: { type: "number", description: "Maximum number of results to return", }, }, required: ["query"], }, returns: { type: "array", description: "Array of search results with relevance scores", }, }, { name: "index", description: "Index a new entry in the vector store", parameters: { type: "object", properties: { content: { type: "string", description: "Content to index", }, metadata: { type: "object", description: "Metadata for the entry", }, }, required: ["content"], }, returns: { type: "object", description: "Result of the indexing operation", }, }, ], }, { name: "sandbox", type: "execution_environment", description: "Secure sandbox for executing code", properties: { languages: { type: "array", description: "Supported programming languages", }, timeout: { type: "number", description: "Maximum execution time in seconds", }, }, methods: [ { name: "execute", description: "Execute code in the sandbox", parameters: { type: "object", properties: { code: { type: "string", description: "Code to execute", }, language: { type: "string", description: "Programming language", }, }, required: ["code", "language"], }, returns: { type: "object", description: "Execution results including stdout, stderr, and any errors", }, }, ], }, ]; // Start the HTTP server await serve(async (req: Request) => { // Handle CORS preflight requests if (req.method === "OPTIONS") { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", }, }); } // Handle discovery endpoint if ( (req.url.endsWith("/discover") || req.url.endsWith("/capabilities")) && req.method === "GET" ) { return new Response( JSON.stringify({ tools, resources, version: "2.0.5", name: "SPARC2 MCP Server", description: "Model Context Protocol server for SPARC2", }), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, }, ); } // Handle list_tools endpoint (legacy) if (req.url.endsWith("/list_tools") && req.method === "GET") { return new Response(JSON.stringify({ tools }), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, }); } // Handle analyze endpoint if (req.url.endsWith("/analyze") && req.method === "POST") { try { const body = await req.json(); const { files, task } = body; if (!files || !Array.isArray(files) || files.length === 0) { return new Response(JSON.stringify({ error: "Files array is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Convert file paths to FileToProcess objects const filesToProcess: FileToProcess[] = []; for (const file of files) { try { const content = await Deno.readTextFile(file); filesToProcess.push({ path: file, originalContent: content, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error reading file ${file}: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to read file: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Use planAndExecute to analyze the files const results = await agent.planAndExecute( `Analyze the following files: ${task}`, filesToProcess, ); // Convert the results to the expected format const formattedResults = results.map((result) => ({ file: result.path, issues: result.originalContent !== result.modifiedContent ? ["Changes suggested"] : [], suggestions: result.originalContent !== result.modifiedContent ? [result.modifiedContent] : [], })); return new Response(JSON.stringify(formattedResults), { headers: { "Content-Type": "application/json" }, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error analyzing code: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to analyze code: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Handle modify endpoint if (req.url.endsWith("/modify") && req.method === "POST") { try { const body = await req.json(); const { files, task } = body; if (!files || !Array.isArray(files) || files.length === 0) { return new Response(JSON.stringify({ error: "Files array is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Convert file paths to FileToProcess objects const filesToProcess: FileToProcess[] = []; for (const file of files) { try { const content = await Deno.readTextFile(file); filesToProcess.push({ path: file, originalContent: content, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error reading file ${file}: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to read file: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Use planAndExecute to modify the files const results = await agent.planAndExecute( `Modify the following files: ${task}`, filesToProcess, ); // Write the modified content back to the files for (const result of results) { if (result.originalContent !== result.modifiedContent) { await Deno.writeTextFile(result.path, result.modifiedContent); } } // Process the results const processedResults = results.map((result) => ({ file: result.path, modified: result.originalContent !== result.modifiedContent, changes: result.originalContent !== result.modifiedContent ? ["File was modified according to suggestions"] : [], })); return new Response(JSON.stringify(processedResults), { headers: { "Content-Type": "application/json" }, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error modifying code: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to modify code: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Handle execute endpoint if (req.url.endsWith("/execute") && req.method === "POST") { try { const body = await req.json(); const { code, language = "javascript" } = body; if (!code) { return new Response(JSON.stringify({ error: "Code is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Execute the code directly using the code interpreter const result = await executeCode(code, { language }); // Format the output let output = ""; if (result.logs.stdout.length > 0) { output += result.logs.stdout.join("\n"); } if (result.logs.stderr.length > 0) { output += "\n\nErrors:\n" + result.logs.stderr.join("\n"); } if (result.error) { output += "\n\nExecution Error:\n" + result.error.value; } // Return the execution result return new Response( JSON.stringify({ result: output || "Execution completed successfully", details: result, }), { headers: { "Content-Type": "application/json" }, }, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error executing code: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to execute code: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Handle search endpoint if (req.url.endsWith("/search") && req.method === "POST") { try { const body = await req.json(); const { query, limit = 10 } = body; if (!query) { return new Response(JSON.stringify({ error: "Search query is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Import the vector search function const { searchVectorStore } = await import("../vector/vectorStore.ts"); // Perform the search const results = await searchVectorStore(query, limit); return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" }, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error searching code: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to search code: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Handle rollback endpoint if (req.url.endsWith("/rollback") && req.method === "POST") { try { const body = await req.json(); const { commit } = body; if (!commit) { return new Response(JSON.stringify({ error: "Commit hash is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Import the rollback function const { rollbackChanges } = await import("../git/gitIntegration.ts"); // Perform the rollback const message = `Rollback to ${commit}`; const result = await rollbackChanges(commit, message); return new Response( JSON.stringify({ commit, success: true, result, }), { headers: { "Content-Type": "application/json" }, }, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error rolling back changes: ${errorMessage}`); return new Response( JSON.stringify({ error: `Failed to roll back changes: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } // Handle checkpoint endpoint if (req.url.endsWith("/checkpoint") && req.method === "POST") { try { const body = await req.json(); const { name } = body; if (!name) { return new Response(JSON.stringify({ error: "Checkpoint name is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Sanitize the name for Git tag const sanitizedName = name.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase().substring(0, 50); // Create a checkpoint const hash = await createCheckpoint(sanitizedName); return new Response( JSON.stringify({ name: sanitizedName, hash: hash, }), { headers: { "Content-Type": "application/json" }, }, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error creating checkpoint: ${errorMessage}`); return new Response( JSON.stringify({ error: `Failed to create checkpoint: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } // Handle config endpoint if (req.url.endsWith("/config") && req.method === "POST") { try { const body = await req.json(); const { action, key, value } = body; if (!action) { return new Response(JSON.stringify({ error: "Action is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Handle different config actions if (action === "get") { // Get a specific config value if (!key) { return new Response(JSON.stringify({ error: "Key is required for get action" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const configValue = Deno.env.get(key) || null; return new Response(JSON.stringify({ key, value: configValue }), { headers: { "Content-Type": "application/json" }, }); } else if (action === "set") { // Set a specific config value if (!key) { return new Response(JSON.stringify({ error: "Key is required for set action" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } if (value === undefined) { return new Response(JSON.stringify({ error: "Value is required for set action" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } // Set the environment variable Deno.env.set(key, String(value)); return new Response(JSON.stringify({ key, value, success: true }), { headers: { "Content-Type": "application/json" }, }); } else if (action === "list") { // List all config values (only safe ones) const safeKeys = [ "DENO_ENV", "DEBUG", "LOG_LEVEL", "VECTOR_STORE_ID", "DIFF_MODE", "PROCESSING_MODE", ]; const config: Record<string, string | null> = {}; for (const key of safeKeys) { config[key] = Deno.env.get(key) || null; } return new Response(JSON.stringify(config), { headers: { "Content-Type": "application/json" }, }); } else { return new Response(JSON.stringify({ error: `Unknown action: ${action}` }), { status: 400, headers: { "Content-Type": "application/json" }, }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); await logError(`Error managing config: ${errorMessage}`); return new Response(JSON.stringify({ error: `Failed to manage config: ${errorMessage}` }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } // Handle unknown endpoints return new Response(JSON.stringify({ error: "Endpoint not found" }), { status: 404, headers: { "Content-Type": "application/json" }, }); }, { port }); await logInfo(`SPARC2 MCP server running on http://localhost:${port}`); }