@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.
- package/LICENSE +13 -0
- package/README.md +476 -0
- package/build/generated/tools.js +9402 -0
- package/build/handler.js +29 -0
- package/build/http-server.js +179 -0
- package/build/index.js +35 -0
- package/build/server.js +33 -0
- package/build/types.js +1 -0
- package/build/utils/apiClient.js +128 -0
- package/build/utils/clientConfig.js +37 -0
- package/build/utils/logger.js +38 -0
- package/build/utils/responseFormatter.js +33 -0
- package/package.json +62 -0
package/build/handler.js
ADDED
|
@@ -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
|
+
});
|
package/build/server.js
ADDED
|
@@ -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
|
+
}
|