feat: Enhance deployment capabilities with direct server deployment tools, email configuration, and comprehensive documentation

This commit is contained in:
ethan.chen
2026-01-07 16:56:31 +08:00
parent 8f8f852ce4
commit 2458bfa111
18 changed files with 1430 additions and 12 deletions

View File

@@ -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...");

View File

@@ -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
View 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
View 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,
};
}
}
);
}

View File

@@ -440,7 +440,7 @@ serve({
module: "ESNext",
lib: ["ES2022"],
moduleResolution: "bundler",
types: ["bun-types"],
types: ["node"],
strict: true,
esModuleInterop: true,
skipLibCheck: true,