UNPKG

@stately-cloud/statelydb-mcp-server

Version:

Model Context Protocol (MCP) Server for interacting with StatelyDB

419 lines (418 loc) 16 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const child_process_1 = require("child_process"); const util_1 = require("util"); const execAsync = (0, util_1.promisify)(child_process_1.exec); const server = new mcp_js_1.McpServer({ name: "statelydb-mcp-server", version: "1.0.0" }); // Create a temporary directory for stately operations async function createTempDirectory() { try { // Generate a unique directory name but don't create it yet const tempDirName = path.join(os.tmpdir(), `statelydb-mcp-${Date.now()}-${Math.floor(Math.random() * 10000)}`); // Let the stately schema init command create the directory await execAsync(`stately schema init ${tempDirName}`); return tempDirName; } catch (error) { console.error('Failed to initialize stately schema:', error); throw new Error(`Failed to initialize stately schema: ${error instanceof Error ? error.message : String(error)}`); } } server.tool("statelydb-validate-schema", "Validate a StatelyDB elastic schema definition.", { schema: zod_1.z.string().describe("The schema definition to validate") }, async ({ schema }) => { // Create a temporary directory with stately schema init const tempDir = await createTempDirectory(); try { // Write the schema to a temporary file const schemaFilePath = path.join(tempDir, 'schema.ts'); fs.writeFileSync(schemaFilePath, schema); // Run the stately CLI to validate the schema const { stdout, stderr } = await execAsync(`stately schema validate ${schemaFilePath}`, { cwd: tempDir }); // Check if validation was successful based on stdout containing "Schema is valid" const isValid = stdout.includes("Schema is valid"); if (isValid) { return { content: [ { type: "text", text: "Schema is valid." } ], isError: false }; } else { return { content: [ { type: "text", text: `Schema is invalid. Error: ${stderr}\nOutput: ${stdout}` } ], isError: true }; } } catch (error) { // Extract both stdout and stderr from the error object if available let errorMessage = error instanceof Error ? error.message : String(error); let stdout = ''; if (error instanceof Error && 'stdout' in error) { stdout = error.stdout || ''; } return { content: [ { type: "text", text: `Failed to validate schema: ${errorMessage}${stdout ? `\n\nCommand output: ${stdout}` : ''}` } ], isError: true }; } finally { // Clean up temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (error) { console.error(`Failed to clean up temporary directory ${tempDir}:`, error); } } }); server.tool("statelydb-validate-migrations", "Validate schema migrations inside of a StatelyDB elastic schema definition.", { schema: zod_1.z.string().describe("The schema definition to validate"), schemaId: zod_1.z.string().describe("The schema ID") }, async ({ schema, schemaId }) => { // Create a temporary directory with stately schema init const tempDir = await createTempDirectory(); try { // Write the schema to a temporary file const schemaFilePath = path.join(tempDir, 'schema.ts'); fs.writeFileSync(schemaFilePath, schema); // Run the stately CLI to validate migrations in dry-run mode const { stdout, stderr } = await execAsync(`stately schema put -s ${schemaId} ${schemaFilePath} --dry-run`, { cwd: tempDir }); // Check if validation was successful const isValid = stdout.includes("Dry Run: Schema was not published.") && !stdout.includes("✘"); return { content: [ { type: "text", text: isValid ? "Migrations are valid." : `Migrations are invalid. Error: ${stderr} Output: ${stdout}` } ], isError: !isValid }; } catch (error) { // Extract both stdout and stderr from the error object if available let errorMessage = error instanceof Error ? error.message : String(error); let stdout = ''; if (error instanceof Error && 'stdout' in error) { stdout = error.stdout || ''; } return { content: [ { type: "text", text: `Failed to validate migrations: ${errorMessage}${stdout ? `\n\nCommand output: ${stdout}` : ''}` } ], isError: true }; } finally { // Clean up temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (error) { console.error(`Failed to clean up temporary directory ${tempDir}:`, error); } } }); server.tool("statelydb-attempt-login", "Initiate StatelyDB login process and get an authorization URL", {}, async () => { try { // Run the stately login command const { stdout, stderr } = await execAsync('stately login'); // Extract the URL from the output const urlMatch = stdout.match(/(https:\/\/oauth\.stately\.cloud\/activate\?user_code=[\w-]+)/i); if (urlMatch && urlMatch[1]) { return { content: [ { type: "text", text: `Please visit this URL to complete the authentication process: ${urlMatch[1]}` } ] }; } else { return { content: [ { type: "text", text: `Login URL not found in output. Full output: ${stdout}${stderr ? `\nError: ${stderr}` : ''}` } ], isError: true }; } } catch (error) { // Extract both stdout and stderr from the error object if available let errorMessage = error instanceof Error ? error.message : String(error); let stdout = ''; if (error instanceof Error && 'stdout' in error) { stdout = error.stdout || ''; } return { content: [ { type: "text", text: `Failed to initiate login: ${errorMessage}${stdout ? `\n\nCommand output: ${stdout}` : ''}` } ], isError: true }; } }); server.tool("statelydb-verify-login", "Verify if the user is logged in to StatelyDB. This command can also tell you what organizations, stores, and schemas you have access to.", {}, async () => { try { // Run the stately whoami command const { stdout, stderr } = await execAsync('stately whoami'); // Check if login is valid const isLoggedIn = stdout.includes("Stately UserID"); return { content: [ { type: "text", text: isLoggedIn ? `You are logged in. Details: ${stdout}` : "You are not logged in." } ], isError: !isLoggedIn }; } catch (error) { return { content: [ { type: "text", text: `Failed to verify login: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); server.tool("statelydb-schema-put", "Publish an elastic schema version definition to StatelyDB", { schema: zod_1.z.string().describe("The schema definition to publish"), schemaId: zod_1.z.string().describe("The schema ID") }, async ({ schema, schemaId }) => { // Create a temporary directory with stately schema init const tempDir = await createTempDirectory(); try { // Write the schema to a temporary file const schemaFilePath = path.join(tempDir, 'schema.ts'); fs.writeFileSync(schemaFilePath, schema); // Run the stately CLI to put the schema const { stdout, stderr } = await execAsync(`stately schema put -s ${schemaId} ${schemaFilePath}`, { cwd: tempDir }); // Check if publish was successful const isPublished = !stderr; return { content: [ { type: "text", text: isPublished ? `Schema published successfully: ${stdout}` : `Failed to publish schema: ${stderr} ${stdout}` } ], isError: !isPublished }; } catch (error) { return { content: [ { type: "text", text: `Failed to publish schema! ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } finally { // Clean up temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (error) { console.error(`Failed to clean up temporary directory ${tempDir}:`, error); } } }); server.tool("statelydb-schema-generate", "Generate client code from a published StatelyDB schema version definition. Supported languages: TypeScript, Python, Ruby, Go.", { schemaId: zod_1.z.string().describe("The schema ID"), language: zod_1.z.enum(["typescript", "python", "ruby", "go"]).describe("The language to generate code for") }, async ({ schemaId, language }) => { // Create a temporary output directory with a name appropriate for the language let outputDir; if (language === "go") { // For Go, use only alphanumeric characters to create a valid package name outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'statelygo')); // Go requires a go.mod file with a valid import path containing at least one period try { // Initialize a go module in the directory with a valid import path await execAsync(`go mod init github.com/stately/schema`, { cwd: outputDir }); } catch (error) { console.error('Failed to initialize Go module:', error); throw new Error('Failed to initialize Go module. Make sure Go is installed and available in your PATH.'); } } else { outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stately-gen-')); } try { // Run the stately CLI to generate code await execAsync(`stately schema generate -s ${schemaId} -l ${language} ${outputDir}`); // Read and collect all generated files const files = collectFiles(outputDir); if (files.length === 0) { return { content: [ { type: "text", text: "No files were generated." } ], isError: true }; } // Format the output const fileListText = files.map(file => `File: ${file.name}\n` + `Contents:\n\`\`\`${getLanguageExtension(file.name)}\n${file.content}\n\`\`\``).join('\n\n'); return { content: [ { type: "text", text: `Generated the following files for language ${language}:\n\n${fileListText}` } ] }; } catch (error) { // Extract both stdout and stderr from the error object if available let errorMessage = error instanceof Error ? error.message : String(error); let stdout = ''; if (error instanceof Error && 'stdout' in error) { stdout = error.stdout || ''; } return { content: [ { type: "text", text: `Failed to generate schema: ${errorMessage}${stdout ? `\n\nCommand output: ${stdout}` : ''}` } ], isError: true }; } finally { // Clean up temporary directory try { fs.rmSync(outputDir, { recursive: true, force: true }); } catch (error) { console.error(`Failed to clean up temporary directory ${outputDir}:`, error); } } }); // Helper function to recursively collect files from a directory function collectFiles(dirPath, basePath = '') { const files = []; const entries = fs.readdirSync(path.join(dirPath, basePath), { withFileTypes: true }); for (const entry of entries) { const relativePath = path.join(basePath, entry.name); if (entry.isDirectory()) { files.push(...collectFiles(dirPath, relativePath)); } else { const filePath = path.join(dirPath, relativePath); const content = fs.readFileSync(filePath, 'utf8'); files.push({ name: relativePath, content }); } } return files; } // Helper function to get the language extension for syntax highlighting function getLanguageExtension(filename) { const ext = path.extname(filename); const extensionMap = { '.ts': 'typescript', '.js': 'javascript', '.py': 'python', '.rb': 'ruby', '.go': 'go', '.json': 'json', '.md': 'markdown' }; return extensionMap[ext] || ''; } const transport = new stdio_js_1.StdioServerTransport(); server.connect(transport).catch(error => { console.error("Failed to start MCP server:", error); process.exit(1); });