Skip to main content
Glama
akutishevsky

LunchMoney MCP Server

delete_tag

Destructive

Delete a tag by ID; fails if in use unless force=true to remove dependencies.

Instructions

Delete a tag. By default fails (HTTP 422) with a structured dependents payload if the tag is in use by transactions or rules. Set force=true to delete and disassociate from those records.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
tagIdYesId of the tag to delete.
forceNoIf true, force deletion even if dependencies exist (irreversible).

Implementation Reference

  • Handler function for the delete_tag tool. Sends a DELETE request to /tags/{tagId} (optionally with ?force=true), handles 204 success, 422 with dependents payload, and other errors.
    server.registerTool(
        "delete_tag",
        {
            description:
                "Delete a tag. By default fails (HTTP 422) with a structured `dependents` payload if the tag is in use by transactions or rules. Set force=true to delete and disassociate from those records.",
            inputSchema: {
                tagId: z.coerce.number().describe("Id of the tag to delete."),
                force: z
                    .boolean()
                    .optional()
                    .describe(
                        "If true, force deletion even if dependencies exist (irreversible).",
                    ),
            },
            annotations: {
                destructiveHint: true,
            },
        },
        async ({ tagId, force }) => {
            try {
                const path = force
                    ? `/tags/${tagId}?force=true`
                    : `/tags/${tagId}`;
                const response = await api.delete(path);
    
                if (response.status === 204) {
                    return successResponse("Tag deleted.");
                }
    
                if (!response.ok) {
                    if (response.status === 422) {
                        return dataResponse(await response.json());
                    }
                    return handleApiError(response, "Failed to delete tag");
                }
    
                return dataResponse(await response.json());
            } catch (error) {
                return catchError(error, "Failed to delete tag");
            }
        },
    );
  • Registration of the delete_tag tool on the MCP server with description, input schema (tagId, force), and annotations.
    server.registerTool(
        "delete_tag",
        {
            description:
                "Delete a tag. By default fails (HTTP 422) with a structured `dependents` payload if the tag is in use by transactions or rules. Set force=true to delete and disassociate from those records.",
            inputSchema: {
                tagId: z.coerce.number().describe("Id of the tag to delete."),
                force: z
                    .boolean()
                    .optional()
                    .describe(
                        "If true, force deletion even if dependencies exist (irreversible).",
                    ),
            },
            annotations: {
                destructiveHint: true,
            },
  • Type definition for DeleteTagWithDependencies, used when the API returns 422 with dependents info.
    export interface DeleteTagWithDependencies {
        tag_name: string;
        dependents: {
            rules: number;
            transactions: number;
        };
    }
  • Parent function registerTagTools that contains all tag tool registrations including delete_tag.
    export function registerTagTools(server: McpServer) {
        server.registerTool(
            "get_all_tags",
            {
                description:
                    "Get a list of all tags associated with the user's account.",
                annotations: {
                    readOnlyHint: true,
                },
            },
            async () => {
                try {
                    const response = await api.get("/tags");
    
                    if (!response.ok) {
                        return handleApiError(response, "Failed to get tags");
                    }
    
                    return dataResponse(await response.json());
                } catch (error) {
                    return catchError(error, "Failed to get tags");
                }
            },
        );
    
        server.registerTool(
            "get_single_tag",
            {
                description: "Get details of a single tag by ID.",
                inputSchema: {
                    tagId: z.coerce
                        .number()
                        .describe(
                            "Id of the tag to query. Call get_all_tags first to discover ids.",
                        ),
                },
                annotations: {
                    readOnlyHint: true,
                },
            },
            async ({ tagId }) => {
                try {
                    const response = await api.get(`/tags/${tagId}`);
    
                    if (!response.ok) {
                        return handleApiError(response, "Failed to get tag");
                    }
    
                    return dataResponse(await response.json());
                } catch (error) {
                    return catchError(error, "Failed to get tag");
                }
            },
        );
    
        server.registerTool(
            "create_tag",
            {
                description: "Create a new tag.",
                inputSchema: {
                    name: z
                        .string()
                        .min(1)
                        .max(100)
                        .describe("Name of the tag. 1-100 characters."),
                    description: z
                        .string()
                        .max(200)
                        .optional()
                        .describe("Optional description. Up to 200 characters."),
                    text_color: z
                        .string()
                        .optional()
                        .describe("Optional text color of the tag."),
                    background_color: z
                        .string()
                        .optional()
                        .describe("Optional background color of the tag."),
                    archived: z
                        .boolean()
                        .optional()
                        .describe("If true, the tag is created archived."),
                },
                annotations: {
                    idempotentHint: false,
                },
            },
            async ({
                name,
                description,
                text_color,
                background_color,
                archived,
            }) => {
                try {
                    const requestBody: Record<string, unknown> = { name };
                    if (description !== undefined)
                        requestBody.description = description;
                    if (text_color !== undefined)
                        requestBody.text_color = normalizeColor(text_color);
                    if (background_color !== undefined)
                        requestBody.background_color =
                            normalizeColor(background_color);
                    if (archived !== undefined) requestBody.archived = archived;
    
                    const response = await api.post("/tags", requestBody);
    
                    if (!response.ok) {
                        return handleApiError(response, "Failed to create tag");
                    }
    
                    return dataResponse(await response.json());
                } catch (error) {
                    return catchError(error, "Failed to create tag");
                }
            },
        );
    
        server.registerTool(
            "update_tag",
            {
                description: "Update properties for an existing tag.",
                inputSchema: {
                    tagId: z.coerce.number().describe("Id of the tag to update."),
                    name: z.string().min(1).max(100).optional(),
                    description: z.string().max(200).nullable().optional(),
                    text_color: z.string().nullable().optional(),
                    background_color: z.string().nullable().optional(),
                    archived: z.boolean().optional(),
                },
                annotations: {
                    idempotentHint: true,
                },
            },
            async ({
                tagId,
                name,
                description,
                text_color,
                background_color,
                archived,
            }) => {
                try {
                    const requestBody: Record<string, unknown> = {};
                    if (name !== undefined) requestBody.name = name;
                    if (description !== undefined)
                        requestBody.description = description;
                    if (text_color !== undefined)
                        requestBody.text_color = normalizeColor(text_color);
                    if (background_color !== undefined)
                        requestBody.background_color =
                            normalizeColor(background_color);
                    if (archived !== undefined) requestBody.archived = archived;
    
                    const response = await api.put(`/tags/${tagId}`, requestBody);
    
                    if (!response.ok) {
                        return handleApiError(response, "Failed to update tag");
                    }
    
                    return dataResponse(await response.json());
                } catch (error) {
                    return catchError(error, "Failed to update tag");
                }
            },
        );
    
        server.registerTool(
            "delete_tag",
            {
                description:
                    "Delete a tag. By default fails (HTTP 422) with a structured `dependents` payload if the tag is in use by transactions or rules. Set force=true to delete and disassociate from those records.",
                inputSchema: {
                    tagId: z.coerce.number().describe("Id of the tag to delete."),
                    force: z
                        .boolean()
                        .optional()
                        .describe(
                            "If true, force deletion even if dependencies exist (irreversible).",
                        ),
                },
                annotations: {
                    destructiveHint: true,
                },
            },
            async ({ tagId, force }) => {
                try {
                    const path = force
                        ? `/tags/${tagId}?force=true`
                        : `/tags/${tagId}`;
                    const response = await api.delete(path);
    
                    if (response.status === 204) {
                        return successResponse("Tag deleted.");
                    }
    
                    if (!response.ok) {
                        if (response.status === 422) {
                            return dataResponse(await response.json());
                        }
                        return handleApiError(response, "Failed to delete tag");
                    }
    
                    return dataResponse(await response.json());
                } catch (error) {
                    return catchError(error, "Failed to delete tag");
                }
            },
        );
    }
  • src/index.ts:8-8 (registration)
    Import of registerTagTools from tags module.
    import { registerTagTools } from "./tools/tags.js";
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark it as destructive (destructiveHint=true). The description adds transparency by detailing the default 422 error with dependents payload and the irreversible nature of force=true, which is valuable beyond the annotation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences front-load the purpose and immediately clarify the key behavioral nuance. No extraneous information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple delete tool with two parameters, the description covers the core behavior, error handling, and the force option. No output schema is needed for a delete operation, so completeness is high.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema covers 100% of parameters (tagId, force). The description adds meaning by explaining the default behavior (fails with dependents) and the effect of force=true (irreversible deletion and disassociation), which the schema does not convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with 'Delete a tag,' clearly stating the action and resource. It further distinguishes behavior from siblings by explaining default failure on dependencies and the force flag, which sets it apart from other tag operations like update_tag.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear when-to-use guidance: default behavior fails with dependents payload, while force=true deletes and disassociates. It does not explicitly mention alternatives but adequately covers the main usage scenarios.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/akutishevsky/lunchmoney-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server