From 23c64c1c69d69b67fc43d4e2270aacda7ccf311e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20Tar=C4=B1m?= <68031925+ismailtrm@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:19:04 +0300 Subject: [PATCH 1/2] feat: add compose stack tools Adds 6 tools for managing Docker Compose stacks in Dokploy: - compose-one: fetch compose stack details - compose-deploy: trigger a deployment - compose-redeploy: redeploy (pull latest + restart) - compose-start: start a stopped stack - compose-stop: stop a running stack - compose-cleanQueues: clear stuck deployment queues Compose stacks are a first-class resource type in Dokploy alongside Applications, but were previously missing from the MCP server. --- src/mcp/tools/compose/composeCleanQueues.ts | 29 ++++++++++++++++++++ src/mcp/tools/compose/composeDeploy.ts | 28 +++++++++++++++++++ src/mcp/tools/compose/composeOne.ts | 30 +++++++++++++++++++++ src/mcp/tools/compose/composeRedeploy.ts | 28 +++++++++++++++++++ src/mcp/tools/compose/composeStart.ts | 28 +++++++++++++++++++ src/mcp/tools/compose/composeStop.ts | 28 +++++++++++++++++++ src/mcp/tools/compose/index.ts | 6 +++++ src/mcp/tools/index.ts | 2 ++ 8 files changed, 179 insertions(+) create mode 100644 src/mcp/tools/compose/composeCleanQueues.ts create mode 100644 src/mcp/tools/compose/composeDeploy.ts create mode 100644 src/mcp/tools/compose/composeOne.ts create mode 100644 src/mcp/tools/compose/composeRedeploy.ts create mode 100644 src/mcp/tools/compose/composeStart.ts create mode 100644 src/mcp/tools/compose/composeStop.ts create mode 100644 src/mcp/tools/compose/index.ts diff --git a/src/mcp/tools/compose/composeCleanQueues.ts b/src/mcp/tools/compose/composeCleanQueues.ts new file mode 100644 index 0000000..ed500b1 --- /dev/null +++ b/src/mcp/tools/compose/composeCleanQueues.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; +import apiClient from "../../../utils/apiClient.js"; +import { ResponseFormatter } from "../../../utils/responseFormatter.js"; +import { createTool } from "../toolFactory.js"; + +export const composeCleanQueues = createTool({ + name: "compose-cleanQueues", + description: + "Cleans the deployment queues for a compose stack in Dokploy. Use this to unstick a stuck deployment.", + schema: z.object({ + composeId: z + .string() + .describe("The ID of the compose stack to clean queues for."), + }), + annotations: { + title: "Clean Compose Queues", + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + handler: async (input) => { + const response = await apiClient.post("/compose.cleanQueues", input); + + return ResponseFormatter.success( + `Compose stack "${input.composeId}" queues cleaned successfully`, + response.data + ); + }, +}); diff --git a/src/mcp/tools/compose/composeDeploy.ts b/src/mcp/tools/compose/composeDeploy.ts new file mode 100644 index 0000000..2330eaf --- /dev/null +++ b/src/mcp/tools/compose/composeDeploy.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import apiClient from "../../../utils/apiClient.js"; +import { ResponseFormatter } from "../../../utils/responseFormatter.js"; +import { createTool } from "../toolFactory.js"; + +export const composeDeploy = createTool({ + name: "compose-deploy", + description: "Deploys a compose stack in Dokploy.", + schema: z.object({ + composeId: z + .string() + .describe("The ID of the compose stack to deploy."), + }), + annotations: { + title: "Deploy Compose Stack", + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + handler: async (input) => { + const response = await apiClient.post("/compose.deploy", input); + + return ResponseFormatter.success( + `Compose stack "${input.composeId}" deployment started successfully`, + response.data + ); + }, +}); diff --git a/src/mcp/tools/compose/composeOne.ts b/src/mcp/tools/compose/composeOne.ts new file mode 100644 index 0000000..461f405 --- /dev/null +++ b/src/mcp/tools/compose/composeOne.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import apiClient from "../../../utils/apiClient.js"; +import { ResponseFormatter } from "../../../utils/responseFormatter.js"; +import { createTool } from "../toolFactory.js"; + +export const composeOne = createTool({ + name: "compose-one", + description: "Retrieves details of a specific compose stack in Dokploy.", + schema: z.object({ + composeId: z + .string() + .describe("The ID of the compose stack to retrieve."), + }), + annotations: { + title: "Get Compose Stack", + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + handler: async (input) => { + const response = await apiClient.get("/compose.one", { + params: input, + }); + + return ResponseFormatter.success( + `Compose stack "${input.composeId}" retrieved successfully`, + response.data + ); + }, +}); diff --git a/src/mcp/tools/compose/composeRedeploy.ts b/src/mcp/tools/compose/composeRedeploy.ts new file mode 100644 index 0000000..cef78fb --- /dev/null +++ b/src/mcp/tools/compose/composeRedeploy.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import apiClient from "../../../utils/apiClient.js"; +import { ResponseFormatter } from "../../../utils/responseFormatter.js"; +import { createTool } from "../toolFactory.js"; + +export const composeRedeploy = createTool({ + name: "compose-redeploy", + description: "Redeploys a compose stack in Dokploy (pulls latest images and restarts).", + schema: z.object({ + composeId: z + .string() + .describe("The ID of the compose stack to redeploy."), + }), + annotations: { + title: "Redeploy Compose Stack", + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + handler: async (input) => { + const response = await apiClient.post("/compose.redeploy", input); + + return ResponseFormatter.success( + `Compose stack "${input.composeId}" redeployment started successfully`, + response.data + ); + }, +}); diff --git a/src/mcp/tools/compose/composeStart.ts b/src/mcp/tools/compose/composeStart.ts new file mode 100644 index 0000000..f17c3bd --- /dev/null +++ b/src/mcp/tools/compose/composeStart.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import apiClient from "../../../utils/apiClient.js"; +import { ResponseFormatter } from "../../../utils/responseFormatter.js"; +import { createTool } from "../toolFactory.js"; + +export const composeStart = createTool({ + name: "compose-start", + description: "Starts a compose stack in Dokploy.", + schema: z.object({ + composeId: z + .string() + .describe("The ID of the compose stack to start."), + }), + annotations: { + title: "Start Compose Stack", + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + handler: async (input) => { + const response = await apiClient.post("/compose.start", input); + + return ResponseFormatter.success( + `Compose stack "${input.composeId}" started successfully`, + response.data + ); + }, +}); diff --git a/src/mcp/tools/compose/composeStop.ts b/src/mcp/tools/compose/composeStop.ts new file mode 100644 index 0000000..0b1bbe8 --- /dev/null +++ b/src/mcp/tools/compose/composeStop.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import apiClient from "../../../utils/apiClient.js"; +import { ResponseFormatter } from "../../../utils/responseFormatter.js"; +import { createTool } from "../toolFactory.js"; + +export const composeStop = createTool({ + name: "compose-stop", + description: "Stops a running compose stack in Dokploy.", + schema: z.object({ + composeId: z + .string() + .describe("The ID of the compose stack to stop."), + }), + annotations: { + title: "Stop Compose Stack", + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + handler: async (input) => { + const response = await apiClient.post("/compose.stop", input); + + return ResponseFormatter.success( + `Compose stack "${input.composeId}" stopped successfully`, + response.data + ); + }, +}); diff --git a/src/mcp/tools/compose/index.ts b/src/mcp/tools/compose/index.ts new file mode 100644 index 0000000..bcd0d3d --- /dev/null +++ b/src/mcp/tools/compose/index.ts @@ -0,0 +1,6 @@ +export { composeOne } from "./composeOne.js"; +export { composeDeploy } from "./composeDeploy.js"; +export { composeRedeploy } from "./composeRedeploy.js"; +export { composeStart } from "./composeStart.js"; +export { composeStop } from "./composeStop.js"; +export { composeCleanQueues } from "./composeCleanQueues.js"; diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index 5a9e1ac..91ba2a0 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -1,4 +1,5 @@ import * as applicationTools from "./application/index.js"; +import * as composeTools from "./compose/index.js"; import * as domainTools from "./domain/index.js"; import * as mysqlTools from "./mysql/index.js"; import * as postgresTools from "./postgres/index.js"; @@ -7,6 +8,7 @@ import * as projectTools from "./project/index.js"; export const allTools = [ ...Object.values(projectTools), ...Object.values(applicationTools), + ...Object.values(composeTools), ...Object.values(domainTools), ...Object.values(mysqlTools), ...Object.values(postgresTools), From 1d7fc8cf9f422001dfd61263138b017c562b491a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20Tar=C4=B1m?= <68031925+ismailtrm@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:33:00 +0300 Subject: [PATCH 2/2] fix: add .min(1) to composeId and readOnlyHint to composeOne --- src/mcp/tools/compose/composeCleanQueues.ts | 1 + src/mcp/tools/compose/composeDeploy.ts | 1 + src/mcp/tools/compose/composeOne.ts | 2 ++ src/mcp/tools/compose/composeRedeploy.ts | 1 + src/mcp/tools/compose/composeStart.ts | 1 + src/mcp/tools/compose/composeStop.ts | 1 + 6 files changed, 7 insertions(+) diff --git a/src/mcp/tools/compose/composeCleanQueues.ts b/src/mcp/tools/compose/composeCleanQueues.ts index ed500b1..50e2d61 100644 --- a/src/mcp/tools/compose/composeCleanQueues.ts +++ b/src/mcp/tools/compose/composeCleanQueues.ts @@ -10,6 +10,7 @@ export const composeCleanQueues = createTool({ schema: z.object({ composeId: z .string() + .min(1) .describe("The ID of the compose stack to clean queues for."), }), annotations: { diff --git a/src/mcp/tools/compose/composeDeploy.ts b/src/mcp/tools/compose/composeDeploy.ts index 2330eaf..96446d7 100644 --- a/src/mcp/tools/compose/composeDeploy.ts +++ b/src/mcp/tools/compose/composeDeploy.ts @@ -9,6 +9,7 @@ export const composeDeploy = createTool({ schema: z.object({ composeId: z .string() + .min(1) .describe("The ID of the compose stack to deploy."), }), annotations: { diff --git a/src/mcp/tools/compose/composeOne.ts b/src/mcp/tools/compose/composeOne.ts index 461f405..5c29422 100644 --- a/src/mcp/tools/compose/composeOne.ts +++ b/src/mcp/tools/compose/composeOne.ts @@ -9,10 +9,12 @@ export const composeOne = createTool({ schema: z.object({ composeId: z .string() + .min(1) .describe("The ID of the compose stack to retrieve."), }), annotations: { title: "Get Compose Stack", + readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, diff --git a/src/mcp/tools/compose/composeRedeploy.ts b/src/mcp/tools/compose/composeRedeploy.ts index cef78fb..59a1bc9 100644 --- a/src/mcp/tools/compose/composeRedeploy.ts +++ b/src/mcp/tools/compose/composeRedeploy.ts @@ -9,6 +9,7 @@ export const composeRedeploy = createTool({ schema: z.object({ composeId: z .string() + .min(1) .describe("The ID of the compose stack to redeploy."), }), annotations: { diff --git a/src/mcp/tools/compose/composeStart.ts b/src/mcp/tools/compose/composeStart.ts index f17c3bd..59d9f05 100644 --- a/src/mcp/tools/compose/composeStart.ts +++ b/src/mcp/tools/compose/composeStart.ts @@ -9,6 +9,7 @@ export const composeStart = createTool({ schema: z.object({ composeId: z .string() + .min(1) .describe("The ID of the compose stack to start."), }), annotations: { diff --git a/src/mcp/tools/compose/composeStop.ts b/src/mcp/tools/compose/composeStop.ts index 0b1bbe8..d1e7c16 100644 --- a/src/mcp/tools/compose/composeStop.ts +++ b/src/mcp/tools/compose/composeStop.ts @@ -9,6 +9,7 @@ export const composeStop = createTool({ schema: z.object({ composeId: z .string() + .min(1) .describe("The ID of the compose stack to stop."), }), annotations: {