Add repository file write tools (create, update, delete) #23

Open
opened 2026-05-10 19:31:08 +00:00 by jbr870 · 0 comments
Owner

Summary

Add MCP tools for creating, updating, and deleting files in a repository, completing the file CRUD surface started by #12 (which delivered the read side).

Background

Issue #12 added get_file_contents, list_directory, and get_repo_tree — all read-only. The Forgejo API (inherited from Gitea) also exposes write endpoints on the same /repos/{owner}/{repo}/contents/{filepath} path, but the MCP server doesn't wrap them. Today, any agent that needs to modify repo files has to ask the user to do it manually, push via git, or use some other out-of-band method.

Forgejo API endpoints

The upstream API provides three individual file-mutation endpoints plus a batch endpoint. All live under /api/v1/repos/{owner}/{repo}/contents/.

1. Create file — POST /repos/{owner}/{repo}/contents/{filepath}

Creates a new file. Fails if the file already exists.

Request body (CreateFileOptions):

  • content (string, required) — file content, base64-encoded
  • message (string, optional) — commit message; Forgejo generates a default if omitted
  • branch (string, optional) — target branch; defaults to the repo's default branch
  • new_branch (string, optional) — create a new branch from branch and commit there
  • author / committer (object with name + email, optional)
  • dates (object, optional) — override author/committer timestamps
  • signoff (bool, optional)

Response: FileResponse — contains the created file's content metadata (path, sha, size, etc.) and the commit object.

2. Update file — PUT /repos/{owner}/{repo}/contents/{filepath}

Updates an existing file. Requires the current blob SHA for optimistic concurrency.

Request body (UpdateFileOptions):

  • Everything from CreateFileOptions, plus:
  • sha (string, required) — the blob SHA of the file being replaced (from get_file_contents)
  • from_path (string, optional) — if set, moves/renames the file from from_path to the filepath in the URL

Response: FileResponse.

3. Delete file — DELETE /repos/{owner}/{repo}/contents/{filepath}

Deletes a file. Also requires the current blob SHA.

Request body (DeleteFileOptions):

  • sha (string, required) — blob SHA of the file to delete
  • message (string, optional)
  • branch (string, optional)
  • new_branch (string, optional)
  • author / committer (optional)
  • dates (optional)
  • signoff (optional)

Response: FileDeleteResponse — contains commit but content is null.

4. Batch modify files — POST /repos/{owner}/{repo}/contents (no filepath)

Applies multiple file operations in a single commit.

Request body (ChangeFilesOptions):

  • files (array of ChangeFileOperation, required) — each operation has:
    • operation"create", "update", or "delete"
    • path (string) — file path
    • content (string, base64) — for create/update
    • sha (string) — for update/delete
    • from_path (string, optional) — for rename/move
  • Plus the shared message, branch, new_branch, author, committer, dates, signoff fields

Response: FilesResponse — array of file results plus a single commit.

Proposed tools

create_file

create_file(
    owner: str,
    repo: str,
    filepath: str,
    content: str,           # plain text — MCP server base64-encodes before sending
    message: str | None,     # commit message
    branch: str | None,      # target branch
    new_branch: str | None,  # create new branch and commit there
    instance: str | None
) -> dict

update_file

update_file(
    owner: str,
    repo: str,
    filepath: str,
    content: str,           # plain text — MCP server base64-encodes
    sha: str,               # current blob SHA (from get_file_contents)
    message: str | None,
    branch: str | None,
    new_branch: str | None,
    from_path: str | None,  # rename/move source path
    instance: str | None
) -> dict

delete_file

delete_file(
    owner: str,
    repo: str,
    filepath: str,
    sha: str,               # current blob SHA (from get_file_contents)
    message: str | None,
    branch: str | None,
    new_branch: str | None,
    instance: str | None
) -> dict

Design decisions

Plain text content, not base64

The MCP server should accept content as plain text and base64-encode it before sending to the Forgejo API. This mirrors the read side: issue #2 established the pattern of decoding base64 content from Forgejo before returning it to the MCP client. Agents work with text, not base64.

For binary files, a separate content_base64 parameter could be added later, or the tool could accept a flag — but binary file creation via MCP is an edge case and can be deferred.

SHA as explicit parameter

Both update_file and delete_file require the blob SHA of the current file. This is by design in the Forgejo API (optimistic concurrency control). The agent gets the SHA from a prior get_file_contents call. The tool docstrings should make this workflow explicit: "call get_file_contents first to obtain the current sha".

Omit author/committer/dates/signoff

These fields are optional in the API and default to the authenticated user. For a first implementation, omitting them keeps the tools simple. They can be added as optional parameters later if a use case emerges.

Nice-to-have (separate issue if pursued)

  • modify_files — batch endpoint wrapper. Useful for atomic multi-file commits (e.g. updating a config file and its corresponding test). More complex parameter shape; worth deferring until the single-file tools are stable.
  • Binary content supportcontent_base64 parameter for non-text files.

Why this matters

  • Completes file CRUD. Issue #12 delivered the R; this adds C, U, D. An agent can now inspect and modify repo files end-to-end without leaving the MCP.
  • Unblocks agentic repo workflows. Creating config files, updating READMEs, adding CI workflows, seeding new repos — all currently require git access or the web UI.
  • Wiki scaffold bootstrapping. The wiki scaffold (project file) could be applied to new repos programmatically: create initial pages, update _Sidebar, etc. — though wiki pages have their own API, the same pattern applies to any file-based setup.
  • Pairs with from_path for file moves. The update endpoint's from_path parameter enables rename/move operations, which are otherwise impossible via the contents API.

Scope

  • src/tools/repos.py — add create_file, update_file, delete_file tools
  • src/forgejo_client.py — add corresponding client methods
  • Tests for all three tools (unit + integration against sandbox repo)
  • Tool docstrings with explicit SHA-workflow guidance
  • #12 — Add repository file/content read tools (delivered, closed)
  • #2 — get_wiki_page should return decoded markdown, not base64 (established the decode-on-read pattern)
## Summary Add MCP tools for creating, updating, and deleting files in a repository, completing the file CRUD surface started by #12 (which delivered the read side). ## Background Issue #12 added `get_file_contents`, `list_directory`, and `get_repo_tree` — all read-only. The Forgejo API (inherited from Gitea) also exposes write endpoints on the same `/repos/{owner}/{repo}/contents/{filepath}` path, but the MCP server doesn't wrap them. Today, any agent that needs to modify repo files has to ask the user to do it manually, push via git, or use some other out-of-band method. ## Forgejo API endpoints The upstream API provides three individual file-mutation endpoints plus a batch endpoint. All live under `/api/v1/repos/{owner}/{repo}/contents/`. ### 1. Create file — `POST /repos/{owner}/{repo}/contents/{filepath}` Creates a new file. Fails if the file already exists. **Request body** (`CreateFileOptions`): - `content` (string, **required**) — file content, base64-encoded - `message` (string, optional) — commit message; Forgejo generates a default if omitted - `branch` (string, optional) — target branch; defaults to the repo's default branch - `new_branch` (string, optional) — create a new branch from `branch` and commit there - `author` / `committer` (object with `name` + `email`, optional) - `dates` (object, optional) — override author/committer timestamps - `signoff` (bool, optional) **Response**: `FileResponse` — contains the created file's `content` metadata (path, sha, size, etc.) and the `commit` object. ### 2. Update file — `PUT /repos/{owner}/{repo}/contents/{filepath}` Updates an existing file. Requires the current blob SHA for optimistic concurrency. **Request body** (`UpdateFileOptions`): - Everything from `CreateFileOptions`, plus: - `sha` (string, **required**) — the blob SHA of the file being replaced (from `get_file_contents`) - `from_path` (string, optional) — if set, moves/renames the file from `from_path` to the `filepath` in the URL **Response**: `FileResponse`. ### 3. Delete file — `DELETE /repos/{owner}/{repo}/contents/{filepath}` Deletes a file. Also requires the current blob SHA. **Request body** (`DeleteFileOptions`): - `sha` (string, **required**) — blob SHA of the file to delete - `message` (string, optional) - `branch` (string, optional) - `new_branch` (string, optional) - `author` / `committer` (optional) - `dates` (optional) - `signoff` (optional) **Response**: `FileDeleteResponse` — contains `commit` but `content` is null. ### 4. Batch modify files — `POST /repos/{owner}/{repo}/contents` (no filepath) Applies multiple file operations in a single commit. **Request body** (`ChangeFilesOptions`): - `files` (array of `ChangeFileOperation`, **required**) — each operation has: - `operation` — `"create"`, `"update"`, or `"delete"` - `path` (string) — file path - `content` (string, base64) — for create/update - `sha` (string) — for update/delete - `from_path` (string, optional) — for rename/move - Plus the shared `message`, `branch`, `new_branch`, `author`, `committer`, `dates`, `signoff` fields **Response**: `FilesResponse` — array of file results plus a single commit. ## Proposed tools ### `create_file` ``` create_file( owner: str, repo: str, filepath: str, content: str, # plain text — MCP server base64-encodes before sending message: str | None, # commit message branch: str | None, # target branch new_branch: str | None, # create new branch and commit there instance: str | None ) -> dict ``` ### `update_file` ``` update_file( owner: str, repo: str, filepath: str, content: str, # plain text — MCP server base64-encodes sha: str, # current blob SHA (from get_file_contents) message: str | None, branch: str | None, new_branch: str | None, from_path: str | None, # rename/move source path instance: str | None ) -> dict ``` ### `delete_file` ``` delete_file( owner: str, repo: str, filepath: str, sha: str, # current blob SHA (from get_file_contents) message: str | None, branch: str | None, new_branch: str | None, instance: str | None ) -> dict ``` ## Design decisions ### Plain text content, not base64 The MCP server should accept `content` as plain text and base64-encode it before sending to the Forgejo API. This mirrors the read side: issue #2 established the pattern of decoding base64 content from Forgejo before returning it to the MCP client. Agents work with text, not base64. For binary files, a separate `content_base64` parameter could be added later, or the tool could accept a flag — but binary file creation via MCP is an edge case and can be deferred. ### SHA as explicit parameter Both `update_file` and `delete_file` require the blob SHA of the current file. This is by design in the Forgejo API (optimistic concurrency control). The agent gets the SHA from a prior `get_file_contents` call. The tool docstrings should make this workflow explicit: "call `get_file_contents` first to obtain the current `sha`". ### Omit author/committer/dates/signoff These fields are optional in the API and default to the authenticated user. For a first implementation, omitting them keeps the tools simple. They can be added as optional parameters later if a use case emerges. ## Nice-to-have (separate issue if pursued) - **`modify_files`** — batch endpoint wrapper. Useful for atomic multi-file commits (e.g. updating a config file and its corresponding test). More complex parameter shape; worth deferring until the single-file tools are stable. - **Binary content support** — `content_base64` parameter for non-text files. ## Why this matters - **Completes file CRUD.** Issue #12 delivered the R; this adds C, U, D. An agent can now inspect and modify repo files end-to-end without leaving the MCP. - **Unblocks agentic repo workflows.** Creating config files, updating READMEs, adding CI workflows, seeding new repos — all currently require git access or the web UI. - **Wiki scaffold bootstrapping.** The wiki scaffold (project file) could be applied to new repos programmatically: create initial pages, update `_Sidebar`, etc. — though wiki pages have their own API, the same pattern applies to any file-based setup. - **Pairs with `from_path` for file moves.** The update endpoint's `from_path` parameter enables rename/move operations, which are otherwise impossible via the contents API. ## Scope - `src/tools/repos.py` — add `create_file`, `update_file`, `delete_file` tools - `src/forgejo_client.py` — add corresponding client methods - Tests for all three tools (unit + integration against sandbox repo) - Tool docstrings with explicit SHA-workflow guidance ## Related - #12 — Add repository file/content read tools (delivered, closed) - #2 — get_wiki_page should return decoded markdown, not base64 (established the decode-on-read pattern)
Sign in to join this conversation.
No labels
status/paused
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jbr870/forgejo-mcp-server#23
No description provided.