Sign In

@masonator/coolify-mcp

Package Overview
Dependencies
Maintainers
1
Versions
80
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@masonator/coolify-mcp - npm Package Compare versions

Comparing version
2.17.0
to
2.18.0
+161
-0
dist/__tests__/elicitation.test.js

@@ -794,1 +794,162 @@ /**

});
describe('elicitation: credential deletes (#315)', () => {
it('asks before deleting a private key, and says it is unrecoverable', async () => {
const h = await harness(decline);
const client = h.server['client'];
jest
.spyOn(client, 'getPrivateKey')
.mockResolvedValue({ uuid: 'key-1', name: 'deploy-key' });
const del = jest.spyOn(client, 'deletePrivateKey').mockResolvedValue({});
const text = await h.call('private_keys', { action: 'delete', uuid: 'key-1' });
// The reason this delete is guarded and the routine ones are not: the key
// material is write-only in Coolify, so there is no undo.
expect(h.prompts[0]).toContain('Delete SSH private key "deploy-key"');
expect(h.prompts[0]).toContain('not recoverable');
expect(del).not.toHaveBeenCalled();
expect(text).toContain('Nothing was changed');
await h.close();
});
it('asks before deleting a cloud token', async () => {
const h = await harness(accept);
const client = h.server['client'];
jest
.spyOn(client, 'getCloudToken')
.mockResolvedValue({ uuid: 'ct-1', name: 'hetzner-prod' });
const del = jest.spyOn(client, 'deleteCloudToken').mockResolvedValue({});
await h.call('cloud_tokens', { action: 'delete', uuid: 'ct-1' });
expect(h.prompts[0]).toContain('Delete cloud-provider token "hetzner-prod"');
expect(del).toHaveBeenCalled();
await h.close();
});
it('counts the applications sourced from a GitHub app before deleting it', async () => {
const h = await harness(decline);
const client = h.server['client'];
jest.spyOn(client, 'listApplications').mockResolvedValue([
{ uuid: 'a', name: 'api', source_id: 7, source_type: 'App\\Models\\GithubApp' },
{ uuid: 'b', name: 'web', source_id: 7, source_type: 'App\\Models\\GithubApp' },
// Same numeric id, different source type — must not be counted.
{ uuid: 'c', name: 'gl', source_id: 7, source_type: 'App\\Models\\GitlabApp' },
{ uuid: 'd', name: 'pub', source_id: null, source_type: null },
]);
jest
.spyOn(client, 'listGitHubApps')
.mockResolvedValue([{ id: 7, name: 'my-installation' }]);
const del = jest.spyOn(client, 'deleteGitHubApp').mockResolvedValue({});
await h.call('github_apps', { action: 'delete', id: 7 });
expect(h.prompts[0]).toContain('Delete GitHub app "my-installation"');
expect(h.prompts[0]).toContain('2 applications');
expect(h.prompts[0]).toContain('lose their deploy source');
expect(h.prompts[0]).not.toContain('gl');
expect(del).not.toHaveBeenCalled();
await h.close();
});
it('says so plainly when no applications are sourced from the GitHub app', async () => {
const h = await harness(accept);
const client = h.server['client'];
jest.spyOn(client, 'listApplications').mockResolvedValue([]);
jest.spyOn(client, 'listGitHubApps').mockResolvedValue([{ id: 7, name: 'idle' }]);
const del = jest.spyOn(client, 'deleteGitHubApp').mockResolvedValue({});
await h.call('github_apps', { action: 'delete', id: 7 });
// Still asks — deleting an installation is not undoable from Coolify's
// side either — but the prompt is honest that nothing currently breaks.
expect(h.prompts[0]).toContain('No applications are currently sourced from it');
expect(del).toHaveBeenCalled();
await h.close();
});
it('leaves the routine deletes unguarded, by decision not omission', async () => {
const h = await harness(accept);
const client = h.server['client'];
const delStorage = jest
.spyOn(client, 'deleteApplicationStorage')
.mockResolvedValue({});
await h.call('storages', {
action: 'delete',
resource: 'application',
uuid: 'app-1',
storage_uuid: 'st-1',
});
// #315's boundary: prompting on every delete is how prompts stop being
// read. If this test starts failing because storages grew a guard, that
// should be a deliberate revisit of the boundary, not a side effect.
expect(h.prompts).toEqual([]);
expect(delStorage).toHaveBeenCalled();
await h.close();
});
});
describe('elicitation: #315 review round', () => {
it('counts an app whose source_type is absent rather than reassuring falsely', async () => {
const h = await harness(decline);
const client = h.server['client'];
jest.spyOn(client, 'listApplications').mockResolvedValue([
// source_id matches but the instance did not serialise source_type —
// the field is absent from the vendored spec and only live-verified on
// 4.1.2, so this response shape is plausible on other versions.
{ uuid: 'a', name: 'api', source_id: 7 },
]);
jest.spyOn(client, 'listGitHubApps').mockResolvedValue([{ id: 7, name: 'inst' }]);
jest.spyOn(client, 'deleteGitHubApp').mockResolvedValue({});
await h.call('github_apps', { action: 'delete', id: 7 });
// Excluding on a missing type would print "No applications are currently
// sourced from it" — the most reassuring sentence this dialog can emit —
// for a delete that breaks the app. Over-counting asks harder instead.
expect(h.prompts[0]).toContain('1 application (api)');
await h.close();
});
it('still asks, carrying the label, when the github_apps pre-flight fails', async () => {
const h = await harness(decline);
const client = h.server['client'];
// The only summarize in the codebase making two calls in Promise.all —
// the most ways to reject, on exactly the flaky-Coolify days when the
// guard matters most.
jest.spyOn(client, 'listApplications').mockRejectedValue(new Error('coolify unreachable'));
jest.spyOn(client, 'listGitHubApps').mockResolvedValue([]);
const del = jest.spyOn(client, 'deleteGitHubApp').mockResolvedValue({});
await h.call('github_apps', { action: 'delete', id: 7 });
expect(h.prompts[0]).toContain('Delete a GitHub app installation');
expect(h.prompts[0]).toContain('Could not load the details first');
expect(del).not.toHaveBeenCalled();
await h.close();
});
it('guards a key-material replacement exactly like a delete', async () => {
const h = await harness(decline);
const client = h.server['client'];
jest
.spyOn(client, 'getPrivateKey')
.mockResolvedValue({ uuid: 'key-1', name: 'deploy-key' });
const update = jest.spyOn(client, 'updatePrivateKey').mockResolvedValue({});
await h.call('private_keys', {
action: 'update',
uuid: 'key-1',
private_key: '-----BEGIN OPENSSH PRIVATE KEY-----\nnew material',
});
// Overwriting key material IS deleting the old key: Coolify never returns
// key material, so the previous value is exactly as gone either way.
expect(h.prompts[0]).toContain('Replace the key material');
expect(h.prompts[0]).toContain('not recoverable');
expect(update).not.toHaveBeenCalled();
await h.close();
});
it('lets a rename through without a prompt', async () => {
const h = await harness(accept);
const update = jest
.spyOn(h.server['client'], 'updatePrivateKey')
.mockResolvedValue({});
await h.call('private_keys', { action: 'update', uuid: 'key-1', name: 'renamed' });
// A rename destroys nothing; prompting on it would be pure fatigue.
expect(h.prompts).toEqual([]);
expect(update).toHaveBeenCalledWith('key-1', { name: 'renamed', description: undefined });
await h.close();
});
it('names the consequence even when no applications are sourced', async () => {
const h = await harness(accept);
const client = h.server['client'];
jest.spyOn(client, 'listApplications').mockResolvedValue([]);
jest.spyOn(client, 'listGitHubApps').mockResolvedValue([{ id: 7, name: 'idle' }]);
jest.spyOn(client, 'deleteGitHubApp').mockResolvedValue({});
await h.call('github_apps', { action: 'delete', id: 7 });
// Every other prompt names what the reader loses; this one must too —
// re-creating and re-installing the app on GitHub is real work.
expect(h.prompts[0]).toContain('not undoable from Coolify');
await h.close();
});
});
+1
-1

@@ -59,3 +59,3 @@ /**

* `elicitation` without implementing the handler, or a proxy that drops the
* request, makes nine tools permanently unusable with no way out but
* request, makes every guarded tool permanently unusable with no way out but
* downgrading the package. The resulting error ("could not confirm with the

@@ -62,0 +62,0 @@ * user") does not point at the client, which makes it hard to diagnose from the

@@ -58,3 +58,3 @@ /**

* `elicitation` without implementing the handler, or a proxy that drops the
* request, makes nine tools permanently unusable with no way out but
* request, makes every guarded tool permanently unusable with no way out but
* downgrading the package. The resulting error ("could not confirm with the

@@ -61,0 +61,0 @@ * user") does not point at the client, which makes it hard to diagnose from the

{
"name": "@masonator/coolify-mcp",
"scope": "@masonator",
"version": "2.17.0",
"version": "2.18.0",
"mcpName": "io.github.StuMason/coolify",

@@ -6,0 +6,0 @@ "description": "MCP server for Coolify — 44 optimized tools for infrastructure management, diagnostics, and documentation search",

@@ -12,4 +12,6 @@ # Coolify MCP Server

📖 **Full docs: [coolify-mcp.stumason.dev](https://coolify-mcp.stumason.dev)** — install guide, tools reference, architecture, security model, v3 roadmap.
📖 **[coolify-mcp.stumason.dev](https://coolify-mcp.stumason.dev)** — what it does, how to install it, and why it is safe to point at production.
This README is the full reference: every tool, every gotcha, every parameter.
## Install

@@ -47,3 +49,3 @@

Behind Cloudflare Access or an auth proxy? Add `--header "Key: Value"` args (repeatable). Cursor, multiple Coolify instances, and proxy setups are covered in the [install guide](https://coolify-mcp.stumason.dev/guide/installation).
Behind Cloudflare Access or an auth proxy? Add `--header "Key: Value"` args (repeatable). The same config works in Cursor, Claude Code and any other MCP client, and can be repeated for multiple Coolify instances.

@@ -77,3 +79,3 @@ ## Tools

Full reference with parameters and examples: [tools docs](https://coolify-mcp.stumason.dev/tools/).
Every tool takes an `action` parameter — run one with no arguments and it lists what it accepts.

@@ -98,3 +100,3 @@ ## Design

Confirmation is asked for on `stop_all_apps`, `redeploy_project`, `restart_project_apps`, `system disable_api`, application / database / service / project / environment deletes, and `bulk_env_update` across more than three apps. Deleting a resource spells out whether its **persistent volumes** go with it — `delete_volumes` defaults to `true` upstream, so leaving the flag unset is the destructive choice, not the cautious one.
Confirmation is asked for on `stop_all_apps`, `redeploy_project`, `restart_project_apps`, `system disable_api`, application / database / service / project / environment deletes, the credential deletes (`private_keys`, `cloud_tokens`, `github_apps` — none recoverable from Coolify once gone), and `bulk_env_update` across more than three apps. Routine deletes (storages, scheduled tasks, individual env vars, backup schedules) deliberately stay unprompted — a dialog on every delete is how dialogs stop being read. Deleting a resource spells out whether its **persistent volumes** go with it — `delete_volumes` defaults to `true` upstream, so leaving the flag unset is the destructive choice, not the cautious one.

@@ -119,3 +121,3 @@ Prompts are skipped where there is nothing to confirm: an emergency stop on an idle estate, or a redeploy of an empty project, just runs.

Details: [security model](https://coolify-mcp.stumason.dev/concepts/security).
Destructive operations also ask a human first — see [Ask before it hurts](#ask-before-it-hurts) above.

@@ -154,4 +156,16 @@ ## Coolify version compatibility

Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and the [contributor docs](https://coolify-mcp.stumason.dev/contributing/adding-tools).
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and the architecture notes in [CLAUDE.md](CLAUDE.md).
## Work with me
I'm Stu Mason. I build MCP servers, AI integrations and agentic systems for agencies, SMEs and enterprise — this repo is what that work looks like in the open.
- **An MCP server for your product** — give Claude, Cursor and every other AI client a proper way into your API, like this one.
- **Answers from your own stuff** — AI that answers from your documents and data, with the receipts, instead of guessing. Can stay on your own servers.
- **Work that runs itself** — jobs on a schedule that sort, check and report, with a person signing off before anything goes out.
White-label under your own name if you're an agency. And if a job doesn't need AI, I'll say so before anyone's paid for anything.
📮 [hey@stumason.dev](mailto:hey@stumason.dev) · [stumason.dev](https://stumason.dev) · [coolify-mcp.stumason.dev](https://coolify-mcp.stumason.dev/#hire)
## Links

@@ -158,0 +172,0 @@

Sorry, the diff of this file is too big to display