@dokploy/mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ import apiClient from "./utils/apiClient.js";
2
+ import { createLogger } from "./utils/logger.js";
3
+ import { ResponseFormatter } from "./utils/responseFormatter.js";
4
+ const logger = createLogger("ToolHandler");
5
+ export function createHandler(tool) {
6
+ return async (input) => {
7
+ try {
8
+ logger.info(`Executing tool: ${tool.name}`, { input });
9
+ const response = tool.method === "GET"
10
+ ? await apiClient.get(tool.path, { params: input })
11
+ : await apiClient.post(tool.path, input);
12
+ return ResponseFormatter.success(`${tool.name} completed successfully`, response.data);
13
+ }
14
+ catch (error) {
15
+ logger.error(`Tool execution failed: ${tool.name}`, {
16
+ error: error instanceof Error ? error.message : "Unknown error",
17
+ });
18
+ if (error instanceof Error) {
19
+ if (error.message.includes("401") || error.message.includes("Unauthorized")) {
20
+ return ResponseFormatter.error(`Authentication failed for ${tool.name}`, "Please check your DOKPLOY_API_KEY configuration");
21
+ }
22
+ if (error.message.includes("404") || error.message.includes("Not Found")) {
23
+ return ResponseFormatter.error("Resource not found", `The requested resource for ${tool.name} could not be found`);
24
+ }
25
+ }
26
+ return ResponseFormatter.error(`Failed to execute ${tool.name}`, `Error: ${error instanceof Error ? error.message : "Unknown error"}`);
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
3
+ import { serve } from "@hono/node-server";
4
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
7
+ import { Hono } from "hono";
8
+ import { createServer } from "./server.js";
9
+ import { createLogger } from "./utils/logger.js";
10
+ const PORT = 3000;
11
+ const logger = createLogger("MCP-HTTP-Server");
12
+ const jsonrpcError = (code, message) => ({
13
+ jsonrpc: "2.0",
14
+ error: { code, message },
15
+ id: null,
16
+ });
17
+ // When MCP transport takes over the raw Node response, we must prevent
18
+ // Hono/@hono/node-server from trying to write its own response headers.
19
+ // We return a Promise that NEVER resolves — the underlying Node response
20
+ // is already being handled by the MCP transport, so Hono must not touch it.
21
+ function neverResolve() {
22
+ return new Promise(() => { });
23
+ }
24
+ export async function main() {
25
+ const app = new Hono();
26
+ const transports = {
27
+ streamable: {},
28
+ sse: {},
29
+ };
30
+ // Health check
31
+ app.get("/health", (c) => c.json({ status: "ok", timestamp: new Date().toISOString() }));
32
+ // Modern Streamable HTTP - POST
33
+ app.post("/mcp", async (c) => {
34
+ const { incoming, outgoing } = c.env;
35
+ try {
36
+ const sessionId = c.req.header("mcp-session-id");
37
+ let transport;
38
+ const body = await c.req.json();
39
+ if (sessionId && transports.streamable[sessionId]) {
40
+ transport = transports.streamable[sessionId];
41
+ }
42
+ else if (!sessionId && isInitializeRequest(body)) {
43
+ transport = new StreamableHTTPServerTransport({
44
+ sessionIdGenerator: () => randomUUID(),
45
+ onsessioninitialized: (sid) => {
46
+ transports.streamable[sid] = transport;
47
+ logger.info("New MCP session initialized", { sessionId: sid });
48
+ },
49
+ });
50
+ transport.onclose = () => {
51
+ if (transport.sessionId) {
52
+ logger.info("MCP session closed", { sessionId: transport.sessionId });
53
+ delete transports.streamable[transport.sessionId];
54
+ }
55
+ };
56
+ const server = createServer();
57
+ await server.connect(transport);
58
+ }
59
+ else {
60
+ return c.json(jsonrpcError(-32000, "Bad Request: No valid session ID or invalid initialization request"), 400);
61
+ }
62
+ await transport.handleRequest(incoming, outgoing, body);
63
+ return neverResolve();
64
+ }
65
+ catch (error) {
66
+ logger.error("Error handling HTTP request", {
67
+ error: error instanceof Error ? error.message : String(error),
68
+ });
69
+ return c.json(jsonrpcError(-32603, "Internal server error"), 500);
70
+ }
71
+ });
72
+ // Modern Streamable HTTP - GET (SSE notifications)
73
+ app.get("/mcp", async (c) => {
74
+ const { incoming, outgoing } = c.env;
75
+ const sessionId = c.req.header("mcp-session-id");
76
+ if (!sessionId || !transports.streamable[sessionId]) {
77
+ return c.json(jsonrpcError(-32000, "Invalid or missing session ID"), 400);
78
+ }
79
+ try {
80
+ const transport = transports.streamable[sessionId];
81
+ await transport.handleRequest(incoming, outgoing);
82
+ return neverResolve();
83
+ }
84
+ catch (error) {
85
+ logger.error("Error handling GET request", {
86
+ error: error instanceof Error ? error.message : String(error),
87
+ });
88
+ return c.json(jsonrpcError(-32603, "Internal server error"), 500);
89
+ }
90
+ });
91
+ // Modern Streamable HTTP - DELETE (session termination)
92
+ app.delete("/mcp", async (c) => {
93
+ const { incoming, outgoing } = c.env;
94
+ const sessionId = c.req.header("mcp-session-id");
95
+ if (!sessionId || !transports.streamable[sessionId]) {
96
+ return c.json(jsonrpcError(-32000, "Invalid or missing session ID"), 400);
97
+ }
98
+ try {
99
+ const transport = transports.streamable[sessionId];
100
+ await transport.handleRequest(incoming, outgoing);
101
+ if (transports.streamable[sessionId]) {
102
+ logger.info("MCP session terminated", { sessionId });
103
+ delete transports.streamable[sessionId];
104
+ }
105
+ return neverResolve();
106
+ }
107
+ catch (error) {
108
+ logger.error("Error handling DELETE request", {
109
+ error: error instanceof Error ? error.message : String(error),
110
+ });
111
+ return c.json(jsonrpcError(-32603, "Internal server error"), 500);
112
+ }
113
+ });
114
+ // Legacy SSE endpoint
115
+ app.get("/sse", async (c) => {
116
+ const { outgoing } = c.env;
117
+ try {
118
+ const transport = new SSEServerTransport("/messages", outgoing);
119
+ transports.sse[transport.sessionId] = transport;
120
+ outgoing.on("close", () => {
121
+ logger.info("Legacy SSE session closed", { sessionId: transport.sessionId });
122
+ delete transports.sse[transport.sessionId];
123
+ });
124
+ const server = createServer();
125
+ await server.connect(transport);
126
+ logger.info("New legacy SSE session initialized", { sessionId: transport.sessionId });
127
+ return neverResolve();
128
+ }
129
+ catch (error) {
130
+ logger.error("Error handling SSE request", {
131
+ error: error instanceof Error ? error.message : String(error),
132
+ });
133
+ return c.json(jsonrpcError(-32603, "Internal server error"), 500);
134
+ }
135
+ });
136
+ // Legacy message endpoint
137
+ app.post("/messages", async (c) => {
138
+ const { incoming, outgoing } = c.env;
139
+ try {
140
+ const sessionId = c.req.query("sessionId");
141
+ if (!sessionId) {
142
+ return c.json(jsonrpcError(-32000, "sessionId query parameter is required"), 400);
143
+ }
144
+ const transport = transports.sse[sessionId];
145
+ if (!transport) {
146
+ return c.json(jsonrpcError(-32000, "No transport found for sessionId"), 400);
147
+ }
148
+ const body = await c.req.json();
149
+ await transport.handlePostMessage(incoming, outgoing, body);
150
+ return neverResolve();
151
+ }
152
+ catch (error) {
153
+ logger.error("Error handling legacy message request", {
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ return c.json(jsonrpcError(-32603, "Internal server error"), 500);
157
+ }
158
+ });
159
+ serve({ fetch: app.fetch, port: PORT }, () => {
160
+ logger.info("MCP Dokploy server started", {
161
+ port: PORT,
162
+ protocols: ["Streamable HTTP (MCP 2025-03-26)", "Legacy SSE (MCP 2024-11-05)"],
163
+ endpoints: {
164
+ modern: `http://localhost:${PORT}/mcp`,
165
+ legacy: `http://localhost:${PORT}/sse`,
166
+ health: `http://localhost:${PORT}/health`,
167
+ },
168
+ });
169
+ });
170
+ }
171
+ if (import.meta.url === `file://${process.argv[1]}`) {
172
+ main().catch((error) => {
173
+ logger.error("Fatal error occurred", {
174
+ error: error instanceof Error ? error.message : String(error),
175
+ stack: error instanceof Error ? error.stack : undefined,
176
+ });
177
+ process.exit(1);
178
+ });
179
+ }
package/build/index.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createServer } from "./server.js";
4
+ import { createLogger } from "./utils/logger.js";
5
+ const logger = createLogger("MCP-Entry");
6
+ async function main() {
7
+ // Check if running in HTTP mode
8
+ const args = process.argv.slice(2);
9
+ const httpMode = args.includes("--http") ||
10
+ args.includes("--sse") ||
11
+ process.env.MCP_TRANSPORT === "http" ||
12
+ process.env.MCP_TRANSPORT === "sse";
13
+ if (httpMode) {
14
+ // Dynamic import to avoid loading Express dependencies when not needed
15
+ const httpModule = await import("./http-server.js");
16
+ if (typeof httpModule.main === "function") {
17
+ return httpModule.main();
18
+ }
19
+ else {
20
+ throw new Error("HTTP server module does not export main function");
21
+ }
22
+ }
23
+ // Default: stdio mode
24
+ const server = createServer();
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ logger.info("MCP Dokploy CLI server running via stdio");
28
+ }
29
+ main().catch((error) => {
30
+ logger.error("Fatal error occurred", {
31
+ error: error.message,
32
+ stack: error.stack,
33
+ });
34
+ process.exit(1);
35
+ });
@@ -0,0 +1,33 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { generatedTools } from "./generated/tools.js";
3
+ import { createHandler } from "./handler.js";
4
+ import { createLogger } from "./utils/logger.js";
5
+ const logger = createLogger("MCP-Server");
6
+ function getEnabledTools() {
7
+ const enabledTags = process.env.DOKPLOY_ENABLED_TAGS;
8
+ if (!enabledTags) {
9
+ return generatedTools;
10
+ }
11
+ const tags = new Set(enabledTags
12
+ .split(",")
13
+ .map((t) => t.trim().toLowerCase())
14
+ .filter(Boolean));
15
+ const filtered = generatedTools.filter((tool) => tags.has(tool.tag.toLowerCase()));
16
+ logger.info("Filtered tools by tags", {
17
+ enabledTags: [...tags],
18
+ total: generatedTools.length,
19
+ loaded: filtered.length,
20
+ });
21
+ return filtered;
22
+ }
23
+ export function createServer() {
24
+ const server = new McpServer({
25
+ name: "dokploy",
26
+ version: "2.0.0",
27
+ });
28
+ const tools = getEnabledTools();
29
+ for (const tool of tools) {
30
+ server.tool(tool.name, tool.description, tool.schema.shape, tool.annotations ?? {}, createHandler(tool));
31
+ }
32
+ return server;
33
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,128 @@
1
+ import axios from "axios";
2
+ import { getClientConfig } from "./clientConfig.js";
3
+ import { createLogger } from "./logger.js";
4
+ // Create logger instance for axios client
5
+ const logger = createLogger("AxiosClient");
6
+ // Get configuration from existing client config
7
+ const config = getClientConfig();
8
+ // Default headers for MCP server context
9
+ const DEFAULT_HEADERS = {
10
+ "Content-Type": "application/json",
11
+ Accept: "application/json",
12
+ "x-api-key": config.authToken, // Use the same auth mechanism as httpClient
13
+ };
14
+ // Create axios instance with configuration from clientConfig
15
+ // Ensure baseURL includes /api prefix for Dokploy API routes
16
+ const baseURL = `${config.dokployUrl.replace(/\/+$/, "")}/api`;
17
+ const apiClient = axios.create({
18
+ baseURL,
19
+ timeout: config.timeout,
20
+ headers: DEFAULT_HEADERS,
21
+ });
22
+ // Request interceptor - Add request timing and logging
23
+ apiClient.interceptors.request.use((config) => {
24
+ // Add request timestamp for performance monitoring
25
+ config.metadata = { startTime: Date.now() };
26
+ logger.debug("Making API request", {
27
+ method: config.method?.toUpperCase(),
28
+ url: config.url,
29
+ baseURL: config.baseURL,
30
+ hasData: !!config.data,
31
+ });
32
+ return config;
33
+ }, (error) => {
34
+ logger.error("Request interceptor error", { error: error.message });
35
+ return Promise.reject(error);
36
+ });
37
+ // Response interceptor - Handle responses and errors with proper logging
38
+ apiClient.interceptors.response.use((response) => {
39
+ // Log response time for performance monitoring
40
+ const metadata = response.config.metadata;
41
+ const startTime = metadata?.startTime;
42
+ if (startTime) {
43
+ const duration = Date.now() - startTime;
44
+ logger.info("API request completed", {
45
+ method: response.config.method?.toUpperCase(),
46
+ url: response.config.url,
47
+ status: response.status,
48
+ duration: `${duration}ms`,
49
+ });
50
+ }
51
+ return response;
52
+ }, (error) => {
53
+ const { response, request, config } = error;
54
+ // Handle different error scenarios with proper logging
55
+ if (response) {
56
+ // Server responded with error status
57
+ handleServerError(response);
58
+ }
59
+ else if (request) {
60
+ // Request was made but no response received
61
+ handleNetworkError(request, config);
62
+ }
63
+ else {
64
+ // Something else happened
65
+ handleUnknownError(error);
66
+ }
67
+ return Promise.reject(error);
68
+ });
69
+ // Helper Functions
70
+ function handleServerError(response) {
71
+ const { status, data, config } = response;
72
+ const errorContext = {
73
+ status,
74
+ method: config?.method?.toUpperCase(),
75
+ url: config?.url,
76
+ message: data?.message || data?.error || "Unknown server error",
77
+ };
78
+ switch (status) {
79
+ case 401:
80
+ logger.error("Authentication failed - Invalid or expired API key", errorContext);
81
+ break;
82
+ case 403:
83
+ logger.error("Access forbidden - Insufficient permissions", errorContext);
84
+ break;
85
+ case 404:
86
+ logger.error("Resource not found", errorContext);
87
+ break;
88
+ case 422:
89
+ logger.error("Validation error", {
90
+ ...errorContext,
91
+ errors: data?.errors,
92
+ });
93
+ break;
94
+ case 500:
95
+ logger.error("Internal server error", errorContext);
96
+ break;
97
+ default:
98
+ logger.error(`Server error (${status})`, errorContext);
99
+ }
100
+ }
101
+ function handleNetworkError(_request, config) {
102
+ logger.error("Network error - Request failed", {
103
+ method: config?.method?.toUpperCase(),
104
+ url: config?.url || "Unknown URL",
105
+ timeout: config?.timeout || "No timeout set",
106
+ error: "No response received from server",
107
+ });
108
+ }
109
+ function handleUnknownError(error) {
110
+ logger.error("Unknown error occurred", {
111
+ error: error.message,
112
+ stack: error.stack,
113
+ });
114
+ }
115
+ // Utility function to update auth token (for MCP context)
116
+ export function setAuthToken(token) {
117
+ // Update default headers for future requests
118
+ apiClient.defaults.headers.common["x-api-key"] = token;
119
+ logger.info("Auth token updated for API client");
120
+ }
121
+ // Utility function to clear auth token
122
+ export function clearAuthToken() {
123
+ // Remove authorization header
124
+ delete apiClient.defaults.headers.common["x-api-key"];
125
+ logger.info("Auth token cleared from API client");
126
+ }
127
+ // Export the configured axios instance
128
+ export default apiClient;
@@ -0,0 +1,37 @@
1
+ class ConfigManager {
2
+ static instance;
3
+ config = null;
4
+ constructor() { }
5
+ static getInstance() {
6
+ if (!ConfigManager.instance) {
7
+ ConfigManager.instance = new ConfigManager();
8
+ }
9
+ return ConfigManager.instance;
10
+ }
11
+ getConfig() {
12
+ if (!this.config) {
13
+ this.config = this.loadConfig();
14
+ }
15
+ return this.config;
16
+ }
17
+ loadConfig() {
18
+ const dokployUrl = process.env.DOKPLOY_URL;
19
+ const authToken = process.env.DOKPLOY_API_KEY;
20
+ if (!dokployUrl) {
21
+ throw new Error("Environment variable DOKPLOY_URL is not defined");
22
+ }
23
+ if (!authToken) {
24
+ throw new Error("Environment variable DOKPLOY_API_KEY is not defined");
25
+ }
26
+ return {
27
+ dokployUrl,
28
+ authToken,
29
+ timeout: parseInt(process.env.DOKPLOY_TIMEOUT || "30000", 10),
30
+ retryAttempts: parseInt(process.env.DOKPLOY_RETRY_ATTEMPTS || "3", 10),
31
+ retryDelay: parseInt(process.env.DOKPLOY_RETRY_DELAY || "1000", 10),
32
+ };
33
+ }
34
+ }
35
+ export function getClientConfig() {
36
+ return ConfigManager.getInstance().getConfig();
37
+ }
@@ -0,0 +1,38 @@
1
+ export var LogLevel;
2
+ (function (LogLevel) {
3
+ LogLevel["ERROR"] = "error";
4
+ LogLevel["WARN"] = "warn";
5
+ LogLevel["INFO"] = "info";
6
+ LogLevel["DEBUG"] = "debug";
7
+ })(LogLevel || (LogLevel = {}));
8
+ class Logger {
9
+ context;
10
+ constructor(context = "MCP-Server") {
11
+ this.context = context;
12
+ }
13
+ log(level, message, meta) {
14
+ const entry = {
15
+ level,
16
+ message,
17
+ timestamp: new Date().toISOString(),
18
+ context: { service: this.context, ...meta },
19
+ };
20
+ // eslint-disable-next-line no-console
21
+ console.error(JSON.stringify(entry));
22
+ }
23
+ error(message, meta) {
24
+ this.log(LogLevel.ERROR, message, meta);
25
+ }
26
+ warn(message, meta) {
27
+ this.log(LogLevel.WARN, message, meta);
28
+ }
29
+ info(message, meta) {
30
+ this.log(LogLevel.INFO, message, meta);
31
+ }
32
+ debug(message, meta) {
33
+ this.log(LogLevel.DEBUG, message, meta);
34
+ }
35
+ }
36
+ export function createLogger(context) {
37
+ return new Logger(context);
38
+ }
@@ -0,0 +1,33 @@
1
+ export class ResponseFormatter {
2
+ static success(message, data) {
3
+ const responseData = {
4
+ success: true,
5
+ message,
6
+ ...(data && typeof data === "object" && data !== null ? { data } : {}),
7
+ };
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: JSON.stringify(responseData, null, 2),
13
+ },
14
+ ],
15
+ };
16
+ }
17
+ static error(message, details) {
18
+ const errorData = {
19
+ success: false,
20
+ error: message,
21
+ ...(details && { details }),
22
+ };
23
+ return {
24
+ isError: true,
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: JSON.stringify(errorData, null, 2),
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@dokploy/mcp",
3
+ "version": "0.0.1",
4
+ "description": "MCP Server for Dokploy API",
5
+ "main": "build/index.js",
6
+ "bin": {
7
+ "dokploy-mcp": "build/index.js"
8
+ },
9
+ "files": [
10
+ "build"
11
+ ],
12
+ "scripts": {
13
+ "build": "pnpm run clean && tsc && chmod 755 build/index.js",
14
+ "dev": "tsc --watch",
15
+ "start:stdio": "node --env-file=.env build/index.js",
16
+ "start:http": "node --env-file=.env build/index.js --http",
17
+ "start:sse": "node --env-file=.env build/index.js --sse",
18
+ "dev:http": "pnpm run build && pnpm run start:http",
19
+ "dev:sse": "pnpm run build && pnpm run start:sse",
20
+ "fetch-openapi": "tsx scripts/fetch-openapi.ts",
21
+ "generate": "tsx scripts/generate-tools.ts",
22
+ "generate:all": "pnpm fetch-openapi && pnpm generate",
23
+ "lint": "biome check",
24
+ "lint:fix": "biome check --fix",
25
+ "format": "biome format --write",
26
+ "format:check": "biome format",
27
+ "type-check": "tsc --noEmit",
28
+ "clean": "rm -rf build",
29
+ "precommit": "biome check && pnpm run type-check",
30
+ "test": "echo \"Error: no test specified\" && exit 1"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Dokploy/mcp.git"
35
+ },
36
+ "keywords": [
37
+ "modelcontextprotocol",
38
+ "mcp",
39
+ "dokploy"
40
+ ],
41
+ "author": "Henrique Andrade <developer@henriqueandrade.dev>",
42
+ "bugs": {
43
+ "url": "https://github.com/Dokploy/mcp/issues"
44
+ },
45
+ "homepage": "https://github.com/Dokploy/mcp#readme",
46
+ "license": "Apache-2.0",
47
+ "type": "module",
48
+ "dependencies": {
49
+ "@hono/node-server": "^1.19.13",
50
+ "@modelcontextprotocol/sdk": "^1.12.0",
51
+ "axios": "^1.9.0",
52
+ "hono": "^4.12.12",
53
+ "zod": "^3.25.28"
54
+ },
55
+ "devDependencies": {
56
+ "@biomejs/biome": "^2.4.10",
57
+ "@types/node": "^22.15.21",
58
+ "json-schema-to-zod": "^2.8.1",
59
+ "tsx": "^4.21.0",
60
+ "typescript": "^5.8.3"
61
+ }
62
+ }