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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ Generate a Heroku authorization token with one of these methods:
- `list_addons` - List all add-ons for all apps or for a specific app.
- `get_addon_info` - Get detailed information about a specific add-on.
- `create_addon` - Provision a new add-on for an app.
- `destroy_addon` - Destroy (delete) an add-on from a specified app. Parameters: `app` (Heroku app name), `addon`
(add-on identifier: name, UUID, or attachment name).

### Maintenance & Logs

Expand Down
Empty file modified bin/heroku-mcp-server.mjs
100644 → 100755
Empty file.
15,778 changes: 10,544 additions & 5,234 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"type": "module",
"dependencies": {
"@heroku/mcp-server": "^1.0.6",
"@heroku/plugin-ai": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.8.0",
"jsonschema": "^1.5.0",
Expand All @@ -19,7 +20,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.23.0",
"@modelcontextprotocol/inspector": "^0.7.0",
"@modelcontextprotocol/inspector": "^0.14.3",
"@types/chai": "^5.2.1",
"@types/mocha": "^10.0.10",
"@types/node": "^20.x",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ teams.registerListTeamsTool(server, herokuRepl);
addons.registerListAddonsTool(server, herokuRepl);
addons.registerGetAddonInfoTool(server, herokuRepl);
addons.registerCreateAddonTool(server, herokuRepl);
addons.registerDestroyAddonTool(server, herokuRepl);
addons.registerListAddonServicesTool(server, herokuRepl);
addons.registerListAddonPlansTool(server, herokuRepl);

Expand Down
59 changes: 58 additions & 1 deletion src/tools/addons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
createAddonOptionsSchema,
listAddonServicesOptionsSchema,
listAddonPlansOptionsSchema,
destroyAddonOptionsSchema,
registerListAddonsTool,
registerGetAddonInfoTool,
registerCreateAddonTool,
registerListAddonServicesTool,
registerListAddonPlansTool
registerListAddonPlansTool,
registerDestroyAddonTool
} from './addons.js';
import { CommandBuilder } from '../utils/command-builder.js';
import { TOOL_COMMAND_MAP } from '../utils/tool-commands.js';
Expand Down Expand Up @@ -328,6 +330,61 @@ describe('addons topic tools', () => {
});
});

describe('registerDestroyAddonTool', () => {
let server: sinon.SinonStubbedInstance<McpServer>;
let herokuRepl: sinon.SinonStubbedInstance<HerokuREPL>;
let toolCallback: Function;

beforeEach(() => {
server = sinon.createStubInstance(McpServer);
herokuRepl = sinon.createStubInstance(HerokuREPL);

server.tool.callsFake((_name, _description, _schema, callback) => {
toolCallback = callback;
return server;
});

registerDestroyAddonTool(server, herokuRepl);
});

afterEach(() => {
sinon.restore();
});

it('registers the tool with correct name and schema', () => {
expect(server.tool.calledOnce).to.be.true;
const call = server.tool.getCall(0);
expect(call.args[0]).to.equal('destroy_addon');
expect(call.args[2]).to.deep.equal(destroyAddonOptionsSchema.shape);
});

it('executes command successfully with required options', async () => {
const expectedOutput = 'Destroying postgresql-curved-12345 on test-app... done\n';
const expectedCommand = new CommandBuilder(TOOL_COMMAND_MAP.DESTROY_ADDON)
.addFlags({ app: 'test-app', confirm: 'test-app' })
.addPositionalArguments({ addon: 'postgresql-curved-12345' })
.build();

herokuRepl.executeCommand.resolves(expectedOutput);

const result = await toolCallback({ app: 'test-app', addon: 'postgresql-curved-12345' }, {});
expect(herokuRepl.executeCommand.calledOnceWith(expectedCommand)).to.be.true;
expect(result).to.deep.equal({
content: [{ type: 'text', text: expectedOutput }]
});
});

it('handles errors from the Heroku CLI', async () => {
const errorOutput = " ▸ Couldn't find that add-on.";
herokuRepl.executeCommand.resolves(errorOutput);

const result = await toolCallback({ app: 'test-app', addon: 'nonexistent-addon' }, {});
expect(result).to.deep.equal({
content: [{ type: 'text', text: errorOutput }]
});
});
});

// Common error handling test for all tools
describe('error handling', () => {
let server: sinon.SinonStubbedInstance<McpServer>;
Expand Down
36 changes: 36 additions & 0 deletions src/tools/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,39 @@ export const registerListAddonPlansTool = (server: McpServer, herokuRepl: Heroku
}
);
};

/**
* Schema for destroying a Heroku add-on.
*/
export const destroyAddonOptionsSchema = z.object({
app: z.string().describe('Target app for add-on destruction. Must have write access.'),
addon: z.string().describe('Add-on identifier: UUID, name (postgresql-curved-12345), or attachment name (DATABASE)')
});

/**
* Type definition for the options used when destroying an add-on.
*/
export type DestroyAddonOptions = z.infer<typeof destroyAddonOptionsSchema>;

/**
* Registers the destroy_addon tool with the MCP server.
*
* @param server - The MCP server instance to register the tool with
* @param herokuRepl - The Heroku REPL instance for executing commands
*/
export const registerDestroyAddonTool = (server: McpServer, herokuRepl: HerokuREPL): void => {
server.tool(
'destroy_addon',
'Destroy (delete) an add-on from a specified app',
destroyAddonOptionsSchema.shape,
async (options: DestroyAddonOptions): Promise<McpToolResponse> => {
const command = new CommandBuilder(TOOL_COMMAND_MAP.DESTROY_ADDON)
.addFlags({ app: options.app, confirm: options.app })
.addPositionalArguments({ addon: options.addon })
.build();

const output = await herokuRepl.executeCommand(command);
return handleCliOutput(output);
}
);
};
1 change: 1 addition & 0 deletions src/utils/tool-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const TOOL_COMMAND_MAP = {
LIST_ADDONS: 'addons',
GET_ADDON_INFO: 'addons:info',
CREATE_ADDON: 'addons:create',
DESTROY_ADDON: 'addons:destroy',
LIST_ADDON_SERVICES: 'addons:services',
LIST_ADDON_PLANS: 'addons:plans',

Expand Down