import { z } from "zod";
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { PloiClient, Site } from "../client.js";
interface PloiConfig {
server_id: number;
site_id: number;
}
async function readPloiConfig(projectPath: string): Promise<PloiConfig | null> {
try {
const configPath = join(projectPath, ".ploi.json");
const content = await readFile(configPath, "utf-8");
const config = JSON.parse(content) as PloiConfig;
if (typeof config.server_id === "number" && typeof config.site_id === "number") {
return config;
}
return null;
} catch {
return null;
}
}
export function registerSiteTools(server: McpServer, client: PloiClient) {
server.tool(
"list_sites",
"List all sites on a server",
{
server_id: z.number().describe("The ID of the server"),
},
async ({ server_id }) => {
const sites = await client.listSites(server_id);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(sites, null, 2),
},
],
};
}
);
server.tool(
"get_site",
"Get details of a specific site",
{
server_id: z.number().describe("The ID of the server"),
site_id: z.number().describe("The ID of the site"),
},
async ({ server_id, site_id }) => {
const site = await client.getSite(server_id, site_id);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(site, null, 2),
},
],
};
}
);
server.tool(
"deploy_site",
"Trigger deployment for a site and wait for it to complete. Returns status when done.",
{
server_id: z.number().describe("The ID of the server"),
site_id: z.number().describe("The ID of the site to deploy"),
},
async ({ server_id, site_id }) => {
await client.deploySite(server_id, site_id);
// Poll until deployment completes (max 5 minutes)
const maxAttempts = 60;
const pollInterval = 5000; // 5 seconds
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const site = await client.getSite(server_id, site_id);
if (site.status === "active") {
return {
content: [
{
type: "text" as const,
text: `✅ Deployment successful!\n\nSite: ${site.domain}\nStatus: ${site.status}`,
},
],
};
}
if (site.status !== "deploying") {
// Some other status (error, etc)
return {
content: [
{
type: "text" as const,
text: `⚠️ Deployment ended with status: ${site.status}\n\nSite: ${site.domain}`,
},
],
};
}
}
return {
content: [
{
type: "text" as const,
text: `⏱️ Deployment still in progress after 5 minutes. Check status manually.`,
},
],
};
}
);
server.tool(
"get_site_logs",
"Get deployment/site logs",
{
server_id: z.number().describe("The ID of the server"),
site_id: z.number().describe("The ID of the site"),
},
async ({ server_id, site_id }) => {
const logs = await client.getSiteLogs(server_id, site_id);
return {
content: [
{
type: "text" as const,
text: logs,
},
],
};
}
);
server.tool(
"suspend_site",
"Suspend a site",
{
server_id: z.number().describe("The ID of the server"),
site_id: z.number().describe("The ID of the site to suspend"),
},
async ({ server_id, site_id }) => {
await client.suspendSite(server_id, site_id);
return {
content: [
{
type: "text" as const,
text: `Site ${site_id} on server ${server_id} has been suspended`,
},
],
};
}
);
server.tool(
"resume_site",
"Resume a suspended site",
{
server_id: z.number().describe("The ID of the server"),
site_id: z.number().describe("The ID of the site to resume"),
},
async ({ server_id, site_id }) => {
await client.resumeSite(server_id, site_id);
return {
content: [
{
type: "text" as const,
text: `Site ${site_id} on server ${server_id} has been resumed`,
},
],
};
}
);
server.tool(
"deploy_project",
"Deploy the current project using .ploi.json config file and wait for completion. Use this when the user says 'deploy' without specifying a site.",
{
project_path: z.string().describe("The path to the project directory containing .ploi.json"),
},
async ({ project_path }) => {
const config = await readPloiConfig(project_path);
if (!config) {
return {
content: [
{
type: "text" as const,
text: `No .ploi.json config found in ${project_path}. Create one with:\n{\n "server_id": YOUR_SERVER_ID,\n "site_id": YOUR_SITE_ID\n}\n\nOr use: "link this project to yourdomain.com"`,
},
],
};
}
const initialSite = await client.getSite(config.server_id, config.site_id);
await client.deploySite(config.server_id, config.site_id);
// Poll until deployment completes (max 5 minutes)
const maxAttempts = 60;
const pollInterval = 5000; // 5 seconds
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const site = await client.getSite(config.server_id, config.site_id);
if (site.status === "active") {
return {
content: [
{
type: "text" as const,
text: `✅ Deployment successful!\n\nSite: ${site.domain}\nStatus: ${site.status}`,
},
],
};
}
if (site.status !== "deploying") {
return {
content: [
{
type: "text" as const,
text: `⚠️ Deployment ended with status: ${site.status}\n\nSite: ${site.domain}`,
},
],
};
}
}
return {
content: [
{
type: "text" as const,
text: `⏱️ Deployment still in progress after 5 minutes for ${initialSite.domain}. Check status manually.`,
},
],
};
}
);
server.tool(
"get_project_deploy_status",
"Check deployment status for the current project using .ploi.json config",
{
project_path: z.string().describe("The path to the project directory containing .ploi.json"),
},
async ({ project_path }) => {
const config = await readPloiConfig(project_path);
if (!config) {
return {
content: [
{
type: "text" as const,
text: `No .ploi.json config found in ${project_path}`,
},
],
};
}
const site = await client.getSite(config.server_id, config.site_id);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
domain: site.domain,
status: site.status,
server_id: config.server_id,
site_id: config.site_id,
}, null, 2),
},
],
};
}
);
server.tool(
"find_site_by_domain",
"Search for a site by domain name across all servers",
{
domain: z.string().describe("The domain name to search for (partial match supported)"),
},
async ({ domain }) => {
const servers = await client.listServers();
const results: Array<{ server_id: number; server_name: string; site: Site }> = [];
for (const server of servers) {
const sites = await client.listSites(server.id);
for (const site of sites) {
if (site.domain.toLowerCase().includes(domain.toLowerCase())) {
results.push({
server_id: server.id,
server_name: server.name,
site,
});
}
}
}
if (results.length === 0) {
return {
content: [
{
type: "text" as const,
text: `No sites found matching "${domain}"`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(results, null, 2),
},
],
};
}
);
server.tool(
"init_project",
"Initialize .ploi.json config for a project by searching for a domain. Use when user wants to link a project to a Ploi site.",
{
project_path: z.string().describe("The path to the project directory"),
domain: z.string().describe("The domain name of the Ploi site to link"),
},
async ({ project_path, domain }) => {
const servers = await client.listServers();
let foundServer: { id: number; name: string } | null = null;
let foundSite: Site | null = null;
for (const server of servers) {
const sites = await client.listSites(server.id);
for (const site of sites) {
if (site.domain.toLowerCase().includes(domain.toLowerCase())) {
foundServer = { id: server.id, name: server.name };
foundSite = site;
break;
}
}
if (foundSite) break;
}
if (!foundSite || !foundServer) {
return {
content: [
{
type: "text" as const,
text: `No site found matching "${domain}". Use list_servers and list_sites to find your site.`,
},
],
};
}
const config = {
server_id: foundServer.id,
site_id: foundSite.id,
};
const configPath = join(project_path, ".ploi.json");
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
return {
content: [
{
type: "text" as const,
text: `Created .ploi.json for ${foundSite.domain}\n\nServer: ${foundServer.name} (${foundServer.id})\nSite: ${foundSite.domain} (${foundSite.id})\n\nYou can now use "deploy" to deploy this project.`,
},
],
};
}
);
}