sourcewizard
Version:
SourceWizard - AI-powered setup wizard for dev tools and libraries with MCP integration
383 lines (382 loc) • 17.1 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { NewAgent } from "../install-agent/new-agent.js";
import { detectRepo } from "../install-agent/repository-detector.js";
import { ProgressServer } from "../cli/progress-server.js";
import fs from 'fs';
import os from 'os';
import path from 'path';
// Global progress server instance
let progressServer = null;
let portFilePath = null;
// Call the events API for installation to get structured responses
async function callEventsAPI(packageName, installationId) {
// Create an agent run in the database first
const agentId = `mcp-${installationId}`;
// Use the same pattern as the web interface - create agent run then call events
const response = await fetch(`http://localhost:3000/api/agent/events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
agent_id: agentId,
operation: 'install',
params: { package: packageName },
cwd: process.cwd()
})
});
if (!response.ok) {
throw new Error(`Events API call failed: ${response.statusText}`);
}
return await response.json();
}
let activeInstallations = new Map();
// Helper function to generate unique installation ID
function generateInstallationId() {
return `install_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Helper function to update metadata in port file
function updatePortFileMetadata(updates) {
if (!portFilePath)
return;
try {
const currentData = JSON.parse(fs.readFileSync(portFilePath, 'utf8'));
const updatedData = { ...currentData, ...updates };
fs.writeFileSync(portFilePath, JSON.stringify(updatedData, null, 2));
}
catch (error) {
console.error('Failed to update port file metadata:', error);
}
}
// Helper function to update installation info in metadata
function updateInstallationInMetadata(installationId, installationUpdates) {
if (!portFilePath)
return;
try {
const currentData = JSON.parse(fs.readFileSync(portFilePath, 'utf8'));
if (!currentData.installations) {
currentData.installations = {};
}
if (!currentData.installations[installationId]) {
currentData.installations[installationId] = {};
}
currentData.installations[installationId] = {
...currentData.installations[installationId],
...installationUpdates
};
fs.writeFileSync(portFilePath, JSON.stringify(currentData, null, 2));
}
catch (error) {
console.error('Failed to update installation metadata:', error);
}
}
const server = new Server({
name: "sourcewizard",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_packages",
description: "Search for packages and code snippets using AI-powered analysis",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query for packages or code snippets",
},
cwd: {
type: "string",
description: "Current working directory path (optional, defaults to process.cwd())",
},
},
required: ["query"],
},
},
{
name: "install_package",
description: "Install and configure a package with AI-guided setup. IMPORTANT: Always call search_packages first to clarify the exact package name before installation. This ensures you're installing the correct package and helps avoid typos or ambiguity in package names.",
inputSchema: {
type: "object",
properties: {
packageName: {
type: "string",
description: "Name of the package to install (should be verified using search_packages first)",
},
cwd: {
type: "string",
description: "Current working directory path (optional, defaults to process.cwd())",
},
},
required: ["packageName"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const cwd = args.cwd || process.cwd();
const projectContext = await detectRepo(cwd);
// Get API key or JWT from environment variables
const apiKey = process.env.SOURCEWIZARD_API_KEY;
if (!apiKey) {
throw new Error("Authentication required. Please set SOURCEWIZARD_API_KEY environment variable or login with 'sourcewizard login'");
}
const agent = new NewAgent({
cwd: cwd,
projectContext,
serverUrl: process.env.SOURCEWIZARD_SERVER_URL || (process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://sourcewizard.ai"),
apiKey,
onStepFinish: ({ text, toolCalls, toolResults, finishReason, usage, stage, description }) => {
// Find the currently active installation for this agent call
// For now, we'll use the most recent installation as there's no direct way to tie agent steps to specific installations
const currentInstallation = Array.from(activeInstallations.values())
.filter(install => install.status === 'installing')
.sort((a, b) => b.startedAt - a.startedAt)[0];
if (currentInstallation && progressServer) {
currentInstallation.stepCounter++;
const isComplete = finishReason === "stop" || finishReason === "length";
const maxSteps = 15; // More realistic estimate for installation steps
console.error(`Progress update for ${currentInstallation.id}: step ${currentInstallation.stepCounter}, text: ${text}, finishReason: ${finishReason}`);
const progressData = {
installationId: currentInstallation.id,
text: text || "Processing...",
toolCalls: toolCalls || [],
toolResults: toolResults || [],
finishReason: isComplete ? "stop" : undefined,
usage: usage || {},
stage: isComplete ? "completed" : (stage || "thinking"),
description: isComplete ? "Package installed successfully" : (description || "Processing your request"),
isComplete,
// Legacy fields for backwards compatibility
step: currentInstallation.stepCounter,
maxSteps,
progress: isComplete ? 100 : Math.min(10 + (currentInstallation.stepCounter * 8), 99)
};
progressServer.updateProgress(progressData);
// Update installation metadata with progress
updateInstallationInMetadata(currentInstallation.id, {
progress: progressData
});
}
},
});
switch (name) {
case "search_packages": {
const query = args.query;
if (typeof query !== "string") {
throw new Error("Query must be a string");
}
const result = await agent.searchPackages(query);
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
result: {
text: result.text,
toolCalls: result.toolCalls,
toolResults: result.toolResults,
finishReason: result.finishReason,
usage: result.usage,
},
}, null, 2),
},
],
};
}
case "install_package": {
const packageName = args.packageName;
if (typeof packageName !== "string") {
throw new Error("Package name must be a string");
}
// Create a new installation with unique ID
const installationId = generateInstallationId();
const installationProgress = {
id: installationId,
packageName,
stepCounter: 0,
status: 'installing',
startedAt: Date.now()
};
activeInstallations.set(installationId, installationProgress);
console.error(`Starting installation of ${packageName} with ID ${installationId}, progress server active: ${!!progressServer}`);
// Update metadata to include this installation
updateInstallationInMetadata(installationId, {
id: installationId,
packageName,
status: 'installing',
startedAt: Date.now()
});
try {
// Install the package using the existing agent with structured progress updates
const result = await agent.installPackage(packageName);
const installation = activeInstallations.get(installationId);
if (installation) {
installation.status = 'completed';
installation.completedAt = Date.now();
// Ensure completion is reported to progress server
if (progressServer) {
console.error(`Explicit completion update for ${installationId}: ${packageName}`);
progressServer.updateProgress({
installationId: installationId,
text: `Installation of ${packageName} completed successfully!`,
toolCalls: [],
toolResults: [],
finishReason: "stop",
usage: {},
stage: "completed",
description: "Package installed successfully",
isComplete: true,
// Legacy fields for backwards compatibility
step: installation.stepCounter || 1,
maxSteps: 15,
progress: 100
});
}
// Update metadata to indicate completion
updateInstallationInMetadata(installationId, {
status: 'completed',
completedAt: Date.now()
});
}
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
result: {
text: result.text,
toolCalls: result.toolCalls,
toolResults: result.toolResults,
finishReason: result.finishReason,
usage: result.usage,
},
}, null, 2),
},
],
};
}
catch (error) {
const installation = activeInstallations.get(installationId);
if (installation) {
installation.status = 'error';
installation.errorAt = Date.now();
installation.error = error instanceof Error ? error.message : String(error);
// Report error through progress server
if (progressServer) {
progressServer.updateProgress({
installationId: installationId,
text: `Installation failed: ${installation.error}`,
toolCalls: [],
toolResults: [],
finishReason: "stop",
usage: {},
stage: "completed",
description: `Installation failed: ${installation.error}`,
isComplete: true,
error: installation.error,
// Legacy fields for backwards compatibility
step: installation.stepCounter,
maxSteps: 15,
progress: 0
});
}
// Update metadata to indicate error
updateInstallationInMetadata(installationId, {
status: 'error',
error: installation.error,
errorAt: Date.now()
});
}
throw error;
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: errorMessage,
}, null, 2),
},
],
isError: true,
};
}
});
// Start the server
export async function main() {
// Start the progress server with dynamic port allocation
progressServer = new ProgressServer(0); // Use 0 for dynamic port
let progressPort;
try {
progressPort = await progressServer.start();
console.error(`Progress server started on http://localhost:${progressPort}`);
// Write the port and metadata to a temp file so CLI can discover it
const portFile = path.join(os.tmpdir(), `sourcewizard-progress-${process.pid}.port`);
const metadata = {
port: progressPort,
pid: process.pid,
startTime: Date.now(),
cwd: process.cwd(),
installations: {}
};
fs.writeFileSync(portFile, JSON.stringify(metadata, null, 2));
portFilePath = portFile;
console.error(`Progress metadata written to: ${portFile}`);
// Clean up port file on exit
process.on('exit', () => {
try {
fs.unlinkSync(portFile);
}
catch (e) {
// Ignore cleanup errors
}
});
process.on('SIGINT', () => {
try {
fs.unlinkSync(portFile);
}
catch (e) {
// Ignore cleanup errors
}
process.exit(0);
});
}
catch (error) {
console.error("Failed to start progress server:", error);
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("SourceWizard MCP server running on stdio");
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
}