Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ The configuration on Windows is slightly different compared to Linux or macOS. U
| `DOKPLOY_URL` | Yes | Your Dokploy server URL (e.g., `https://your-dokploy-server.com`) |
| `DOKPLOY_API_KEY` | Yes | Your Dokploy API authentication token |
| `DOKPLOY_CUSTOM_HEADERS` | No | JSON object of additional upstream request headers. Header names and values must be strings. Reserved headers cannot be set here: `x-api-key`, `content-type`, `accept`. |
| `DOKPLOY_TOOL_PRESET` | No | Predefined toolset to load: `all` (default), `minimal`, `core`, `deploy`, `databases`, or `git`. Useful for clients/providers that struggle with very large tool lists. |
| `DOKPLOY_ENABLED_TAGS` | No | Comma-separated list of tags to filter which tools are loaded (e.g., `project,application,postgres`) |
| `DOKPLOY_DISABLED_TAGS` | No | Comma-separated list of tags to exclude from the selected toolset. Applied after `DOKPLOY_TOOL_PRESET` or `DOKPLOY_ENABLED_TAGS`. |
| `DOKPLOY_TIMEOUT` | No | Request timeout in milliseconds (default: `30000`) |
| `DOKPLOY_RETRY_ATTEMPTS` | No | Number of retry attempts (default: `3`) |
| `DOKPLOY_RETRY_DELAY` | No | Delay between retries in milliseconds (default: `1000`) |
Expand Down Expand Up @@ -423,13 +425,40 @@ This MCP server provides **508 tools** covering the entire Dokploy API, organize

### Tool Filtering

You can limit which tools are loaded by setting the `DOKPLOY_ENABLED_TAGS` environment variable. This is useful when you only need a subset of tools:
By default, the server exposes all Dokploy API tools. Some MCP clients and LLM providers can be slower or less reliable when very large tool lists are sent to the model. You can reduce the loaded tools with presets or tag filters.

Use `DOKPLOY_TOOL_PRESET` for common workflows:

| Preset | Included tags |
|--------|---------------|
| `all` | All tools (default) |
| `minimal` | `project`, `application` |
| `core` | `project`, `server`, `application` |
| `deploy` | `project`, `environment`, `server`, `application`, `domain`, `deployment` |
| `databases` | `postgres`, `redis`, `mysql`, `mariadb`, `mongo`, `libsql` |
| `git` | `github`, `gitlab`, `bitbucket`, `gitea`, `gitProvider`, `registry`, `sshKey` |

```bash
# Recommended starting point for clients/providers sensitive to large toolsets
DOKPLOY_TOOL_PRESET=minimal
```

For exact control, set `DOKPLOY_ENABLED_TAGS`:

```bash
# Only load project, application, and postgres tools
DOKPLOY_ENABLED_TAGS=project,application,postgres
```

You can also remove categories from a preset:

```bash
DOKPLOY_TOOL_PRESET=core
DOKPLOY_DISABLED_TAGS=postgres,redis
```

If `DOKPLOY_ENABLED_TAGS` is set, it takes precedence over `DOKPLOY_TOOL_PRESET`. `DOKPLOY_DISABLED_TAGS` is applied last.

All tools include semantic annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`) to help MCP clients understand their behavior and safety characteristics.

## Architecture
Expand Down Expand Up @@ -499,7 +528,7 @@ npx -y @modelcontextprotocol/inspector npx @dokploy/mcp

3. Verify your `DOKPLOY_URL` and `DOKPLOY_API_KEY` environment variables are correctly set.

4. If too many tools are loading, use `DOKPLOY_ENABLED_TAGS` to filter by category.
4. If too many tools are loading or your provider times out while processing tools, start with `DOKPLOY_TOOL_PRESET=minimal`, then use `DOKPLOY_ENABLED_TAGS` for exact category filtering if needed.

## Contributing

Expand Down
78 changes: 72 additions & 6 deletions src/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

// Mock apiClient before server.ts is imported — it calls getClientConfig() at
// module level which requires DOKPLOY_URL/DOKPLOY_API_KEY env vars.
Expand All @@ -13,6 +13,14 @@ vi.mock("./utils/apiClient.js", () => ({
const { createServer } = await import("./server.js");

describe("MCP server tools/list", () => {
const toolsetEnvVars = ["DOKPLOY_ENABLED_TAGS", "DOKPLOY_DISABLED_TAGS", "DOKPLOY_TOOL_PRESET"];

afterEach(() => {
for (const envVar of toolsetEnvVars) {
delete process.env[envVar];
}
});

async function getToolList() {
const server = createServer();
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
Expand All @@ -29,14 +37,69 @@ describe("MCP server tools/list", () => {
expect(tools.length).toBeGreaterThan(0);
});

it("returns all tools by default", async () => {
const tools = await getToolList();
expect(tools).toHaveLength(524);
});

it("supports DOKPLOY_TOOL_PRESET=minimal for clients sensitive to large toolsets", async () => {
process.env.DOKPLOY_TOOL_PRESET = "minimal";

const tools = await getToolList();
const tags = new Set(tools.map((tool) => tool.name.split("-")[0]));

expect(tools).toHaveLength(40);
expect(tags).toEqual(new Set(["application", "project"]));
});

it("supports DOKPLOY_TOOL_PRESET=core for common application workflows", async () => {
process.env.DOKPLOY_TOOL_PRESET = "core";

const tools = await getToolList();
const tags = new Set(tools.map((tool) => tool.name.split("-")[0]));

expect(tools).toHaveLength(57);
expect(tags).toEqual(new Set(["application", "project", "server"]));
});

it("lets DOKPLOY_ENABLED_TAGS override presets", async () => {
process.env.DOKPLOY_TOOL_PRESET = "core";
process.env.DOKPLOY_ENABLED_TAGS = "project,application";

const tools = await getToolList();
const tags = new Set(tools.map((tool) => tool.name.split("-")[0]));

expect(tools).toHaveLength(40);
expect(tags).toEqual(new Set(["application", "project"]));
});

it("excludes DOKPLOY_DISABLED_TAGS after selecting tools", async () => {
process.env.DOKPLOY_TOOL_PRESET = "deploy";
process.env.DOKPLOY_DISABLED_TAGS = "domain,deployment";

const tools = await getToolList();
const tags = new Set(tools.map((tool) => tool.name.split("-")[0]));

expect(tools).toHaveLength(64);
expect(tags.has("domain")).toBe(false);
expect(tags.has("deployment")).toBe(false);
});

it("falls back to all tools for an unknown preset", async () => {
process.env.DOKPLOY_TOOL_PRESET = "unknown";

const tools = await getToolList();

expect(tools).toHaveLength(524);
});

it("every tool inputSchema has $schema set to draft 2020-12", async () => {
const tools = await getToolList();
for (const tool of tools) {
const schema = tool.inputSchema as Record<string, unknown>;
expect(
schema.$schema,
`Tool "${tool.name}" is missing $schema or has wrong draft`,
).toBe("https://json-schema.org/draft/2020-12/schema");
expect(schema.$schema, `Tool "${tool.name}" is missing $schema or has wrong draft`).toBe(
"https://json-schema.org/draft/2020-12/schema",
);
}
});

Expand All @@ -60,7 +123,10 @@ describe("MCP server tools/list", () => {

for (const tool of tools) {
const found = findNestedSchemaKeys(tool.inputSchema);
expect(found, `Tool "${tool.name}" has nested $schema keys at: ${found.join(", ")}`).toHaveLength(0);
expect(
found,
`Tool "${tool.name}" has nested $schema keys at: ${found.join(", ")}`,
).toHaveLength(0);
}
});

Expand Down
75 changes: 63 additions & 12 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,79 @@ import { createLogger } from "./utils/logger.js";
const logger = createLogger("MCP-Server");

const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
const LARGE_TOOLSET_WARNING_THRESHOLD = 150;

const TOOL_PRESETS = {
all: null,
minimal: "project,application",
core: "project,server,application",
deploy: "project,environment,server,application,domain,deployment",
databases: "postgres,redis,mysql,mariadb,mongo,libsql",
git: "github,gitlab,bitbucket,gitea,gitProvider,registry,sshKey",
} as const;

type ToolPreset = keyof typeof TOOL_PRESETS;

function parseTagList(value: string | undefined): Set<string> {
return new Set(
(value ?? "")
.split(",")
.map((tag) => tag.trim().toLowerCase())
.filter(Boolean),
);
}

function isToolPreset(value: string): value is ToolPreset {
return Object.hasOwn(TOOL_PRESETS, value);
}

function getEnabledTools() {
const enabledTags = process.env.DOKPLOY_ENABLED_TAGS;
const disabledTags = parseTagList(process.env.DOKPLOY_DISABLED_TAGS);
const requestedPreset = process.env.DOKPLOY_TOOL_PRESET?.trim().toLowerCase() || "all";
const preset: ToolPreset = isToolPreset(requestedPreset) ? requestedPreset : "all";

if (!isToolPreset(requestedPreset)) {
logger.warn("Unknown tool preset, falling back to all tools", {
requestedPreset,
availablePresets: Object.keys(TOOL_PRESETS),
});
}

if (!enabledTags) {
return generatedTools;
let selectedTags = parseTagList(enabledTags);
const source = selectedTags.size > 0 ? "enabled-tags" : "preset";

if (selectedTags.size === 0) {
selectedTags = parseTagList(TOOL_PRESETS[preset] ?? undefined);
}

const tags = new Set(
enabledTags
.split(",")
.map((t) => t.trim().toLowerCase())
.filter(Boolean),
);
let filtered =
selectedTags.size > 0
? generatedTools.filter((tool) => selectedTags.has(tool.tag.toLowerCase()))
: generatedTools;

const filtered = generatedTools.filter((tool) => tags.has(tool.tag.toLowerCase()));
if (disabledTags.size > 0) {
filtered = filtered.filter((tool) => !disabledTags.has(tool.tag.toLowerCase()));
}

logger.info("Filtered tools by tags", {
enabledTags: [...tags],
const context = {
total: generatedTools.length,
loaded: filtered.length,
});
source,
preset,
enabledTags: [...selectedTags],
disabledTags: [...disabledTags],
};

logger.info("Loaded tools", context);

if (filtered.length > LARGE_TOOLSET_WARNING_THRESHOLD) {
logger.warn("Large toolset loaded; some MCP clients or LLM providers may time out", {
...context,
recommendation:
"Set DOKPLOY_TOOL_PRESET=minimal or DOKPLOY_ENABLED_TAGS to reduce tool count",
});
}

return filtered;
}
Expand Down