{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "com.fredrin/fredrin",
  "title": "Fredrin",
  "description": "Run a kanban board where every ticket gets its own branch, worktree and AI coding agent.",
  "version": "0.1.0",
  "websiteUrl": "https://www.fredrin.com/docs/mcp",
  "remotes": [
    {
      "type": "streamable-http",
      "url": "https://www.fredrin.com/api/mcp",
      "headers": [
        {
          "name": "Authorization",
          "description": "Bearer fredrin_live_… — a Fredrin API key from Settings → API keys.",
          "isRequired": true,
          "isSecret": true
        }
      ]
    }
  ],
  "_meta": {
    "com.fredrin/mcp": {
      "about": "Fredrin is a desktop kanban platform for vibe coding with parallel AI agents. Every ticket gets its own git branch, worktree and AI Worker, so one person keeps many tickets moving at once. This MCP server exposes that board to any client.",
      "protocolVersion": "2025-06-18",
      "transport": "streamable-http",
      "stateless": true,
      "documentationUrl": "https://www.fredrin.com/docs/mcp",
      "authentication": {
        "type": "bearer",
        "header": "Authorization",
        "keyPrefix": "fredrin_live_",
        "issueKeyUrl": "https://app.fredrin.com/settings/api-keys"
      },
      "tools": [
        {
          "name": "list_workspaces",
          "description": "List workspaces the caller can access. If the API key is workspace-scoped, returns only that workspace.",
          "inputSchema": {
            "type": "object",
            "properties": {},
            "required": [],
            "additionalProperties": false
          }
        },
        {
          "name": "list_projects",
          "description": "List projects in a workspace.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "workspaceId": {
                "type": "string"
              }
            },
            "required": [
              "workspaceId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "create_project",
          "description": "Create a project in a workspace. Color must be a #rrggbb hex string.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "workspaceId": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "color": {
                "type": "string"
              },
              "slug": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "repoUrl": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "techStack": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "workspaceId",
              "name",
              "color"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "update_project",
          "description": "Update project fields (name, slug, color, description, repoUrl, techStack).",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "slug": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "repoUrl": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "techStack": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "projectId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "delete_project",
          "description": "Delete a project. This cascades to all tickets in the project.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              }
            },
            "required": [
              "projectId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "list_statuses",
          "description": "List kanban statuses (columns) in a workspace.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "workspaceId": {
                "type": "string"
              }
            },
            "required": [
              "workspaceId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "list_tickets",
          "description": "List tickets in a project, optionally filtered by status or assignee.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              },
              "statusId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "assigneeId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "includeArchived": {
                "type": "boolean"
              },
              "cursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "limit": {
                "type": "number"
              }
            },
            "required": [
              "projectId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "get_ticket",
          "description": "Fetch a single ticket by id.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              }
            },
            "required": [
              "ticketId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "create_ticket",
          "description": "Create a ticket in a project under a status. Creating a ticket NEVER starts a build: file it under the project's Backlog status and stop there. `statusId` may not name the Running (`building`) column — that column means a Worker is running, and this tool starts none, so naming it is refused. To actually run the ticket, call `move_ticket` into Running afterwards, and only when the human explicitly asked for it to run now; asking for work to be filed is not asking for it to run.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              },
              "statusId": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "createdFromTicketId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dependsOn": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "attachments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "attachmentId": {
                      "type": "string"
                    },
                    "label": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "attachmentId"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "required": [
              "projectId",
              "statusId",
              "title"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "check_ticket_item",
          "description": "Tick or untick ONE checklist item in a ticket's plan. Address it with `item` — the item's stable id (read them off the ticket's `planChecklist`), its full text, or a unique fragment of at least 3 characters — in preference to `index`, its 1-based position: editing the plan renumbers every index below the edit, while an id survives. Give exactly one of the two. An address that names more than one item is refused, never guessed at. This is the ONLY way a checkbox mark can change: `set_ticket_plan` deliberately ignores `[x]` marks in the markdown it is given, because checkbox state is server-owned. The boxes gate nothing — they are the ticket's progress signal: an Implementation plan step (`section: implementation` in `planChecklist`) is ticked the moment its work is done, an acceptance criterion (`section: acceptance`) the moment you have actually met it.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              },
              "item": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "index": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "checked": {
                "type": "boolean"
              }
            },
            "required": [
              "ticketId",
              "checked"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "set_ticket_plan",
          "description": "Write or update the IMPLEMENTATION PLAN for a ticket. ALWAYS use this tool when the user says 'create a plan', 'update the plan', 'write a plan', 'plan this ticket', or anything similar. The plan is stored in the ticket's dedicated `plan` field — it is NOT the same as `description`. Do NOT use `update_ticket` with `description` for plan content. Pass markdown in `plan`. Pass `null` to clear.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              },
              "plan": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "ticketId",
              "plan"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "update_ticket",
          "description": "Update ticket fields (title, description, priority, assignee, due date, labels). For the implementation PLAN, use `set_ticket_plan` instead — DO NOT write plan content into `description`. The `description` field is the user's original ask/scope; the `plan` field is the engineering plan.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "plan": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "priority": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "LOW",
                  "MEDIUM",
                  "HIGH",
                  "URGENT",
                  null
                ]
              },
              "assigneeId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dueDate": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "labelIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "ticketId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "move_ticket",
          "description": "Move a ticket to a status, optionally between two anchor tickets for ordering. Moving into the Running (`building`) column STARTS the ticket's Worker — Running always means a Worker is running. If the Worker cannot be started the move is refused and the ticket stays where it is; the error explains why and what to do. Because it starts real work, move a ticket into Running only when the human explicitly asked for it to run now — filing a ticket is not that instruction.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              },
              "toStatusId": {
                "type": "string"
              },
              "beforeTicketId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "afterTicketId": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "ticketId",
              "toStatusId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "list_comments",
          "description": "List comments on a ticket, oldest first.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              },
              "cursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "limit": {
                "type": "number"
              }
            },
            "required": [
              "ticketId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "create_comment",
          "description": "Add a comment to a ticket. The caller is the author.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "ticketId": {
                "type": "string"
              },
              "body": {
                "type": "string"
              }
            },
            "required": [
              "ticketId",
              "body"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "update_comment",
          "description": "Edit a comment's body. Only the author can edit.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "commentId": {
                "type": "string"
              },
              "body": {
                "type": "string"
              }
            },
            "required": [
              "commentId",
              "body"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "upload_attachment",
          "description": "Upload an image/video/document and get a URL to embed in markdown (e.g. comment bodies). Provide raw bytes as base64 in dataBase64. Pass either workspaceId or ticketId. Returns { id, url, markdown }. Set attachAsPreview:true WITH ticketId to also attach it to that ticket's Artifacts panel as a MANUAL artifact (returns previewId too) — use this to land a chosen design/mockup in the ticket, not just get a markdown URL. Images: png/jpeg/webp/gif up to 5MB. Docs: html/pdf/txt/md/css/json/js/svg up to 5MB. Video: mp4/webm/quicktime up to 25MB.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "workspaceId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ticketId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "mimeType": {
                "type": "string"
              },
              "dataBase64": {
                "type": "string"
              },
              "filename": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "attachAsPreview": {
                "type": "boolean"
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "mimeType",
              "dataBase64"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "delete_comment",
          "description": "Delete a comment. Only the author can delete.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "commentId": {
                "type": "string"
              }
            },
            "required": [
              "commentId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "list_events",
          "description": "List a project's calendar events for ONE year — meetings, launch dates, releases, days off. Read it before proposing a date, so a plan lands on a day nothing else already owns. A repeating event appears ONCE, carrying its `rrule`: its occurrences are expanded from that rule and never stored, and `overrides` holds the occurrences that were separately edited. `truncated: true` means the year hit the 3000-event cap and rows were dropped.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string",
                "description": "The project whose calendar to read."
              },
              "year": {
                "type": "number",
                "description": "Integer calendar year between 1970 and 9999. Defaults to the current UTC year."
              }
            },
            "required": [
              "projectId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "get_event",
          "description": "Fetch ONE calendar event by id. `eventId` is a row: the `id` of a one-off event, or the SERIES id of a repeating one — never the render id of a generated occurrence (`<seriesId>@<start>`), which names no row.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "eventId": {
                "type": "string",
                "description": "The event row to read — a one-off event's id, or a repeating event's series id."
              }
            },
            "required": [
              "eventId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "create_event",
          "description": "Put an entry on a project's calendar — a launch date, a meeting, a day off. `startsAt` takes a date (\"2026-11-08\", an all-day event), an instant carrying its own zone (\"2026-11-08T18:00:00Z\", \"2026-11-08T18:00:00-08:00\"), or a wall clock (\"2026-11-08T18:00\") together with `timeZone` — a wall clock with NO zone is refused rather than guessed at, because reading it as UTC files the event hours away with nothing reporting a problem. Pass `rrule` to make it repeat.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string",
                "description": "The project whose calendar this event goes on."
              },
              "title": {
                "type": "string",
                "description": "the event's name, up to 200 chars"
              },
              "startsAt": {
                "type": "string",
                "description": "\"2026-11-08\" for an all-day event, or an instant with a zone (\"2026-11-08T18:00:00Z\", \"2026-11-08T18:00:00-08:00\"), or a wall clock (\"2026-11-08T18:00\") together with `timeZone`"
              },
              "endsAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "optional, same forms as `startsAt`; a date on a timed event means through the close of that day. null clears it"
              },
              "allDay": {
                "type": "boolean",
                "description": "optional; inferred from `startsAt` (a date is all-day, a clock is not)"
              },
              "timeZone": {
                "type": "string",
                "description": "optional IANA zone (\"America/Los_Angeles\"). Resolves a zone-less `startsAt`, expands a repeat rule, and names the clock in the invite email"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "optional markdown, up to 10000 chars"
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "optional free-text tags"
              },
              "inviteeIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "optional user ids; anyone outside the project's audience is dropped"
              },
              "rrule": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "optional RFC 5545 RRULE value with no prefix, e.g. \"FREQ=WEEKLY;BYDAY=SU\""
              }
            },
            "required": [
              "projectId",
              "title",
              "startsAt"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "update_event",
          "description": "Edit a calendar event. A PARTIAL edit: every field the call does not name keeps what is stored, and an explicit null clears `endsAt`, `description` or `rrule`. `startsAt`/`endsAt` read exactly as they do on `create_event`. To edit ONE occurrence of a repeating event, send the SERIES id plus `scope` and `recurrenceStart` — that occurrence's ORIGINAL start, as a full instant. `list_events` returns the series master (its `startsAt`, `rrule` and `recurrenceTimeZone`) rather than its occurrences, so an occurrence that has never been edited is one you expand from the rule yourself; an occurrence already edited on its own appears in `overrides` carrying its `recurrenceStart` directly.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "eventId": {
                "type": "string",
                "description": "The event row to edit — a one-off event's id, or a repeating event's series id. Never a generated occurrence's render id."
              },
              "title": {
                "type": "string",
                "description": "the event's name, up to 200 chars"
              },
              "startsAt": {
                "type": "string",
                "description": "\"2026-11-08\" for an all-day event, or an instant with a zone (\"2026-11-08T18:00:00Z\", \"2026-11-08T18:00:00-08:00\"), or a wall clock (\"2026-11-08T18:00\") together with `timeZone`"
              },
              "endsAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "optional, same forms as `startsAt`; a date on a timed event means through the close of that day. null clears it"
              },
              "allDay": {
                "type": "boolean",
                "description": "optional; inferred from `startsAt` (a date is all-day, a clock is not)"
              },
              "timeZone": {
                "type": "string",
                "description": "optional IANA zone (\"America/Los_Angeles\"). Resolves a zone-less `startsAt`, expands a repeat rule, and names the clock in the invite email"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "optional markdown, up to 10000 chars"
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "optional free-text tags"
              },
              "inviteeIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "optional user ids; anyone outside the project's audience is dropped"
              },
              "rrule": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "optional RFC 5545 RRULE value with no prefix, e.g. \"FREQ=WEEKLY;BYDAY=SU\""
              },
              "scope": {
                "type": "string",
                "enum": [
                  "this",
                  "following",
                  "all"
                ],
                "description": "optional, defaults to \"all\". Which occurrences of a repeating event the edit reaches"
              },
              "recurrenceStart": {
                "type": "string",
                "description": "the original start of the occurrence being edited, as an instant. Required by scope \"this\" and \"following\""
              }
            },
            "required": [
              "eventId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "delete_event",
          "description": "Delete a calendar event. With no `scope` this removes the whole event, and a repeating event's every occurrence with it. To cancel ONE occurrence — or that one and everything after it — send `scope` plus `recurrenceStart`: that occurrence's ORIGINAL start, as a full instant, expanded from the master's `startsAt` + `rrule` in its `recurrenceTimeZone` (an occurrence already edited on its own carries its `recurrenceStart` in `list_events`' `overrides`). It must match exactly; an instant that names no occurrence cancels nothing.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "eventId": {
                "type": "string",
                "description": "The event row to delete — a one-off event's id, or a repeating event's series id."
              },
              "scope": {
                "type": "string",
                "enum": [
                  "this",
                  "following",
                  "all"
                ],
                "description": "optional, defaults to \"all\". Which occurrences of a repeating event the edit reaches"
              },
              "recurrenceStart": {
                "type": "string",
                "description": "the original start of the occurrence being edited, as an instant. Required by scope \"this\" and \"following\""
              }
            },
            "required": [
              "eventId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "list_apps",
          "description": "List the custom apps installed in a project — name, install slug, kind, and how many callable interfaces each one declares.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              }
            },
            "required": [
              "projectId"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "get_app",
          "description": "Read one installed app: its metadata, the version this project has pinned, the scopes it was granted, and its callable interfaces with their parameters.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              },
              "app": {
                "type": "string",
                "description": "The app's install slug inside this project (what its tab is called), or its app id."
              }
            },
            "required": [
              "projectId",
              "app"
            ],
            "additionalProperties": false
          }
        },
        {
          "name": "call_app",
          "description": "Call one of an app's declared interfaces by name, with parameters, and read the rows it returns.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string"
              },
              "app": {
                "type": "string",
                "description": "The app's install slug inside this project, or its app id."
              },
              "interface": {
                "type": "string",
                "description": "The interface's name, exactly as get_app lists it. An app declares which of its questions can be asked; there is no free-form query."
              },
              "params": {
                "type": "object",
                "additionalProperties": true,
                "description": "The interface's declared parameters, by name. Types must match what get_app reports — a number parameter will not accept a string."
              }
            },
            "required": [
              "projectId",
              "app",
              "interface"
            ],
            "additionalProperties": false
          }
        }
      ]
    }
  }
}