server.ts•5.75 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
McpError,
ErrorCode
} from '@modelcontextprotocol/sdk/types.js';
import { registerTools, handleToolCall } from './tools/index.js';
import { registerPrompts, handlePromptList, handlePromptGet } from './prompts/index.js';
import { validateProjectPath } from './core/path-utils.js';
import { WorkspaceInitializer } from './core/workspace-initializer.js';
import { ProjectRegistry } from './core/project-registry.js';
import { DashboardSessionManager } from './core/dashboard-session.js';
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
export class SpecWorkflowMCPServer {
private server: Server;
private projectPath!: string;
private projectRegistry: ProjectRegistry;
private lang?: string;
constructor() {
// Get version from package.json
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
// Get all registered tools and prompts
const tools = registerTools();
const prompts = registerPrompts();
// Create tools capability object with each tool name
const toolsCapability = tools.reduce((acc, tool) => {
acc[tool.name] = {};
return acc;
}, {} as Record<string, {}>);
this.server = new Server({
name: 'spec-workflow-mcp',
version: packageJson.version
}, {
capabilities: {
tools: toolsCapability,
prompts: {
listChanged: true
}
}
});
this.projectRegistry = new ProjectRegistry();
}
async initialize(projectPath: string, lang?: string) {
this.projectPath = projectPath;
this.lang = lang;
try {
// Validate project path
await validateProjectPath(this.projectPath);
// Initialize workspace
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const workspaceInitializer = new WorkspaceInitializer(this.projectPath, packageJson.version);
await workspaceInitializer.initializeWorkspace();
// Register this project in the global registry
const projectId = await this.projectRegistry.registerProject(this.projectPath, process.pid);
console.error(`Project registered: ${projectId}`);
// Try to get the dashboard URL from session manager
let dashboardUrl: string | undefined = undefined;
try {
const sessionManager = new DashboardSessionManager();
const dashboardSession = await sessionManager.getDashboardSession();
if (dashboardSession) {
dashboardUrl = dashboardSession.url;
}
} catch (error) {
// Dashboard not running, continue without it
}
// Create context for tools
const context = {
projectPath: this.projectPath,
dashboardUrl: dashboardUrl,
lang: this.lang
};
// Register handlers
this.setupHandlers(context);
// Connect to stdio transport
const transport = new StdioServerTransport();
// Handle client disconnection - exit gracefully when transport closes
transport.onclose = async () => {
await this.stop();
process.exit(0);
};
await this.server.connect(transport);
// Monitor stdin for client disconnection (additional safety net)
process.stdin.on('end', async () => {
await this.stop();
process.exit(0);
});
// Handle stdin errors
process.stdin.on('error', async (error) => {
console.error('stdin error:', error);
await this.stop();
process.exit(1);
});
// MCP server initialized successfully
} catch (error) {
throw error;
}
}
private setupHandlers(context: any) {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: registerTools()
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
return await handleToolCall(request.params.name, request.params.arguments || {}, context);
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, error.message);
}
});
// Prompt handlers
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
try {
return await handlePromptList();
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, error.message);
}
});
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
try {
return await handlePromptGet(
request.params.name,
request.params.arguments || {},
context
);
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, error.message);
}
});
}
async stop() {
try {
// Unregister from global registry
try {
await this.projectRegistry.unregisterProject(this.projectPath);
console.error('Project unregistered from global registry');
} catch (error) {
// Ignore errors during cleanup
}
// Stop MCP server
await this.server.close();
} catch (error) {
console.error('Error during shutdown:', error);
// Continue with shutdown even if there are errors
}
}
}