feat: Enhance deployment capabilities with direct server deployment tools, email configuration, and comprehensive documentation
This commit is contained in:
@@ -15,6 +15,7 @@ import { registerCodeReviewTools } from "./tools/programming/codeReview.js";
|
||||
import { registerNASTools } from "./tools/devops/nas.js";
|
||||
import { registerServerTools } from "./tools/devops/server.js";
|
||||
import { registerRouterTools } from "./tools/devops/router.js";
|
||||
import { registerDeployTools } from "./tools/devops/deploy.js";
|
||||
|
||||
import { registerMathTools } from "./tools/family/math.js";
|
||||
import { registerBabyTools } from "./tools/family/baby.js";
|
||||
@@ -24,6 +25,7 @@ import { registerGameTools } from "./tools/hobbies/games.js";
|
||||
|
||||
import { registerNoteTools } from "./tools/common/notes.js";
|
||||
import { registerTaskTools } from "./tools/common/tasks.js";
|
||||
import { registerEmailTools } from "./tools/common/email.js";
|
||||
|
||||
// Register all tool modules
|
||||
logger.info("Registering tools...");
|
||||
@@ -38,6 +40,7 @@ registerCodeReviewTools();
|
||||
registerNASTools();
|
||||
registerServerTools();
|
||||
registerRouterTools();
|
||||
registerDeployTools();
|
||||
|
||||
// Family tools
|
||||
registerMathTools();
|
||||
@@ -50,6 +53,7 @@ registerGameTools();
|
||||
// Common tools
|
||||
registerNoteTools();
|
||||
registerTaskTools();
|
||||
registerEmailTools();
|
||||
|
||||
logger.info("All tools registered. Starting MCP server...");
|
||||
|
||||
|
||||
@@ -23,10 +23,20 @@ export interface RouterConfig {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface EmailConfig {
|
||||
host?: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
from?: string;
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
nas: NASConfig;
|
||||
server: ServerConfig;
|
||||
router: RouterConfig;
|
||||
email: EmailConfig;
|
||||
footballApiKey?: string;
|
||||
gameApiKey?: string;
|
||||
}
|
||||
@@ -57,6 +67,14 @@ class ConfigManager {
|
||||
username: process.env.ROUTER_USERNAME,
|
||||
password: process.env.ROUTER_PASSWORD,
|
||||
},
|
||||
email: {
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT ? parseInt(process.env.EMAIL_PORT) : 587,
|
||||
user: process.env.EMAIL_USER,
|
||||
password: process.env.EMAIL_PASSWORD,
|
||||
from: process.env.EMAIL_FROM,
|
||||
secure: process.env.EMAIL_SECURE === 'true',
|
||||
},
|
||||
footballApiKey: process.env.FOOTBALL_API_KEY,
|
||||
gameApiKey: process.env.GAME_API_KEY,
|
||||
};
|
||||
@@ -78,6 +96,10 @@ class ConfigManager {
|
||||
return this.config.router;
|
||||
}
|
||||
|
||||
getEmailConfig(): EmailConfig {
|
||||
return this.config.email;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
|
||||
206
src/tools/common/email.ts
Normal file
206
src/tools/common/email.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Email sending tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { configManager } from "../../storage/config.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import nodemailer from "nodemailer";
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
|
||||
export function registerEmailTools(): void {
|
||||
// Send email
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "email_send",
|
||||
description:
|
||||
"Send an email via SMTP with support for text, HTML, and attachments",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
to: {
|
||||
type: "string",
|
||||
description: "Recipient email address (required)",
|
||||
},
|
||||
subject: {
|
||||
type: "string",
|
||||
description: "Email subject (required)",
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
description: "Plain text email body (required)",
|
||||
},
|
||||
html: {
|
||||
type: "string",
|
||||
description: "HTML email body (optional, if not provided, text will be used)",
|
||||
},
|
||||
cc: {
|
||||
type: "string",
|
||||
description: "CC recipient email address (optional)",
|
||||
},
|
||||
bcc: {
|
||||
type: "string",
|
||||
description: "BCC recipient email address (optional)",
|
||||
},
|
||||
attachments: {
|
||||
type: "array",
|
||||
description: "Email attachments (optional)",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filename: {
|
||||
type: "string",
|
||||
description: "Attachment filename",
|
||||
},
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to file (if using file path)",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "File content as string (if using content directly)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["to", "subject", "text"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const emailConfig = configManager.getEmailConfig();
|
||||
|
||||
if (!emailConfig.host || !emailConfig.user || !emailConfig.password) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Email configuration not found. Please set EMAIL_HOST, EMAIL_USER, and EMAIL_PASSWORD in environment variables.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const to = args.to as string;
|
||||
const subject = args.subject as string;
|
||||
const text = args.text as string;
|
||||
const html = args.html as string | undefined;
|
||||
const cc = args.cc as string | undefined;
|
||||
const bcc = args.bcc as string | undefined;
|
||||
const attachments = args.attachments as
|
||||
| Array<{ filename: string; path?: string; content?: string }>
|
||||
| undefined;
|
||||
|
||||
// Create transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: emailConfig.host,
|
||||
port: emailConfig.port || 587,
|
||||
secure: emailConfig.secure || false,
|
||||
auth: {
|
||||
user: emailConfig.user,
|
||||
pass: emailConfig.password,
|
||||
},
|
||||
});
|
||||
|
||||
// Process attachments
|
||||
const processedAttachments: any[] = [];
|
||||
if (attachments && attachments.length > 0) {
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.path) {
|
||||
// Use file path
|
||||
if (existsSync(attachment.path)) {
|
||||
processedAttachments.push({
|
||||
filename: attachment.filename,
|
||||
path: attachment.path,
|
||||
});
|
||||
} else {
|
||||
logger.warn(`Attachment file not found: ${attachment.path}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Attachment file not found: ${attachment.path}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} else if (attachment.content) {
|
||||
// Use content directly
|
||||
processedAttachments.push({
|
||||
filename: attachment.filename,
|
||||
content: attachment.content,
|
||||
});
|
||||
} else {
|
||||
logger.warn(
|
||||
`Invalid attachment: missing path or content for ${attachment.filename}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Invalid attachment: missing path or content for ${attachment.filename}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare email options
|
||||
const mailOptions: any = {
|
||||
from: emailConfig.from || emailConfig.user,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
};
|
||||
|
||||
if (html) {
|
||||
mailOptions.html = html;
|
||||
}
|
||||
|
||||
if (cc) {
|
||||
mailOptions.cc = cc;
|
||||
}
|
||||
|
||||
if (bcc) {
|
||||
mailOptions.bcc = bcc;
|
||||
}
|
||||
|
||||
if (processedAttachments.length > 0) {
|
||||
mailOptions.attachments = processedAttachments;
|
||||
}
|
||||
|
||||
// Send email
|
||||
logger.info(`Sending email to ${to} with subject: ${subject}`);
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Email sent successfully!\n\nTo: ${to}\nSubject: ${subject}\nMessage ID: ${info.messageId}${processedAttachments.length > 0 ? `\nAttachments: ${processedAttachments.length}` : ""}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error sending email:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error sending email: ${errorMessage}\n\nPlease check:\n1. Email configuration (EMAIL_HOST, EMAIL_USER, EMAIL_PASSWORD)\n2. SMTP server connection\n3. Recipient email address`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
384
src/tools/devops/deploy.ts
Normal file
384
src/tools/devops/deploy.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* Deployment tools - for direct server deployment
|
||||
* These tools run directly on the server, no SSH needed
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export function registerDeployTools(): void {
|
||||
// Pull latest code and deploy
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "deploy_update",
|
||||
description:
|
||||
"Pull latest code from git and redeploy the MCP server (runs directly on server)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
branch: {
|
||||
type: "string",
|
||||
description: "Git branch to pull from (default: main or master)",
|
||||
default: "main",
|
||||
},
|
||||
rebuild: {
|
||||
type: "boolean",
|
||||
description: "Force rebuild Docker image (default: false)",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const branch = (args.branch as string) || "main";
|
||||
const rebuild = (args.rebuild as boolean) || false;
|
||||
const projectDir = process.cwd();
|
||||
|
||||
logger.info(`Starting deployment update in ${projectDir}`);
|
||||
|
||||
let output = "Deployment Update\n\n";
|
||||
const steps: string[] = [];
|
||||
|
||||
// Step 1: Check if we're in a git repository
|
||||
if (!existsSync(join(projectDir, ".git"))) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Not in a git repository. Please run this from the project directory.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Fetch latest changes
|
||||
try {
|
||||
steps.push("Fetching latest changes from git...");
|
||||
const fetchOutput = execSync("git fetch origin", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
output += `✓ Fetched changes\n`;
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error fetching from git: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 3: Check current branch
|
||||
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
// Step 4: Pull latest code
|
||||
try {
|
||||
steps.push(`Pulling latest code from ${branch}...`);
|
||||
const pullOutput = execSync(`git pull origin ${branch}`, {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
output += `✓ Pulled latest code from ${branch}\n`;
|
||||
output += `\n${pullOutput}\n`;
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error pulling code: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 5: Install dependencies if package.json changed
|
||||
try {
|
||||
steps.push("Checking for dependency updates...");
|
||||
const hasPackageChanges = execSync(
|
||||
"git diff HEAD@{1} HEAD -- package.json bun.lockb",
|
||||
{ cwd: projectDir, encoding: "utf-8" }
|
||||
).trim();
|
||||
|
||||
if (hasPackageChanges) {
|
||||
steps.push("Installing/updating dependencies...");
|
||||
execSync("bun install", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
stdio: "inherit",
|
||||
});
|
||||
output += `✓ Dependencies updated\n`;
|
||||
} else {
|
||||
output += `✓ No dependency changes\n`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Not critical, continue
|
||||
logger.warn("Could not check dependencies:", error);
|
||||
}
|
||||
|
||||
// Step 6: Restart service (if using systemd or PM2)
|
||||
try {
|
||||
steps.push("Restarting MCP server...");
|
||||
|
||||
// Check if running as systemd service
|
||||
const serviceName = process.env.MCP_SERVICE_NAME || "cloud-mcp";
|
||||
try {
|
||||
execSync(`systemctl is-active --quiet ${serviceName}`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
// Service exists and is active, restart it
|
||||
execSync(`sudo systemctl restart ${serviceName}`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
output += `✓ Restarted systemd service: ${serviceName}\n`;
|
||||
} catch {
|
||||
// Not a systemd service, try PM2
|
||||
try {
|
||||
execSync("pm2 list | grep cloud-mcp", {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
execSync("pm2 restart cloud-mcp", {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
output += `✓ Restarted PM2 process: cloud-mcp\n`;
|
||||
} catch {
|
||||
// Not PM2 either, just log that manual restart is needed
|
||||
output += `⚠ Service restart skipped (not running as systemd/PM2)\n`;
|
||||
output += ` Please restart the MCP server manually\n`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("Could not restart service:", error);
|
||||
output += `⚠ Could not auto-restart service\n`;
|
||||
}
|
||||
|
||||
output += `\n✅ Deployment update completed!\n`;
|
||||
output += `\nSteps executed:\n${steps
|
||||
.map((s, i) => `${i + 1}. ${s}`)
|
||||
.join("\n")}`;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: output,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Deployment error:", error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error during deployment: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Check deployment status
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "deploy_status",
|
||||
description:
|
||||
"Check deployment status - git status, last commit, service status",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const projectDir = process.cwd();
|
||||
let output = "Deployment Status\n\n";
|
||||
|
||||
// Git status
|
||||
try {
|
||||
const gitStatus = execSync("git status --short", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const lastCommit = execSync("git log -1 --oneline", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
||||
cwd: projectDir,
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
|
||||
output += `📦 Git Status:\n`;
|
||||
output += `Branch: ${currentBranch}\n`;
|
||||
output += `Last commit: ${lastCommit}\n`;
|
||||
|
||||
// Check if behind remote
|
||||
try {
|
||||
execSync("git fetch origin", { cwd: projectDir });
|
||||
const behind = execSync(
|
||||
`git rev-list HEAD..origin/${currentBranch} --count`,
|
||||
{ cwd: projectDir, encoding: "utf-8" }
|
||||
).trim();
|
||||
|
||||
if (behind !== "0") {
|
||||
output += `⚠ Behind remote by ${behind} commit(s)\n`;
|
||||
} else {
|
||||
output += `✓ Up to date with remote\n`;
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
if (gitStatus) {
|
||||
output += `\nUncommitted changes:\n${gitStatus}\n`;
|
||||
} else {
|
||||
output += `✓ Working directory clean\n`;
|
||||
}
|
||||
} catch (error) {
|
||||
output += `Error checking git status: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}\n`;
|
||||
}
|
||||
|
||||
// Service status
|
||||
output += `\n🔧 Service Status:\n`;
|
||||
const serviceName = process.env.MCP_SERVICE_NAME || "cloud-mcp";
|
||||
|
||||
try {
|
||||
const systemdStatus = execSync(`systemctl is-active ${serviceName}`, {
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
output += `Systemd: ${systemdStatus}\n`;
|
||||
} catch {
|
||||
try {
|
||||
const pm2Status = execSync("pm2 list | grep cloud-mcp", {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
output += `PM2: Running\n`;
|
||||
} catch {
|
||||
output += `Service: Not managed by systemd/PM2\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Process status
|
||||
try {
|
||||
const processInfo = execSync(
|
||||
"ps aux | grep 'bun.*index.ts' | grep -v grep",
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
).trim();
|
||||
if (processInfo) {
|
||||
output += `\nProcess: Running\n`;
|
||||
} else {
|
||||
output += `\nProcess: Not found\n`;
|
||||
}
|
||||
} catch {
|
||||
output += `\nProcess: Unknown\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: output,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error checking status: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// View deployment logs
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "deploy_logs",
|
||||
description: "View deployment logs",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
lines: {
|
||||
type: "number",
|
||||
description: "Number of lines to show (default: 50)",
|
||||
default: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
try {
|
||||
const lines = (args.lines as number) || 50;
|
||||
const logFile = join(process.cwd(), "deploy.log");
|
||||
|
||||
if (existsSync(logFile)) {
|
||||
const content = readFileSync(logFile, "utf-8");
|
||||
const logLines = content.split("\n").slice(-lines).join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Deployment Logs (last ${lines} lines):\n\n${logLines}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No deployment log file found. Logs will be created on first deployment.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error reading logs: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -440,7 +440,7 @@ serve({
|
||||
module: "ESNext",
|
||||
lib: ["ES2022"],
|
||||
moduleResolution: "bundler",
|
||||
types: ["bun-types"],
|
||||
types: ["node"],
|
||||
strict: true,
|
||||
esModuleInterop: true,
|
||||
skipLibCheck: true,
|
||||
|
||||
Reference in New Issue
Block a user