Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

A repository created over MCP is private unless the agent says public (#52)

The repository tool's create action passed its arguments to create_repo as the REST API does, where a repository is public unless private is true, so an agent that left private out published the code it was starting. Over MCP a repository is now private unless the call gives private: false (or visibility: "public"): the MCP server fills in that default before it runs the operation, for the repository tool's create and the older per-operation create_repo tool alike. POST to the REST API keeps its default, public, and the CLI its own, private. The create action's summary in the tool's schema says so, as do the MCP reference and the build-and-ship guide's step that told the agent to pass private: true.

Checked:
cargo test -p g1t-api (209, with a new test that create defaults to private, honours private: false and visibility "public", and leaves other operations' arguments alone); docs build and npm test.
G1t-Session:
ses_01m4ynr1peevvs8zwaqakycv2e
syntaqxcommitted Parent6c6da20Browse files
4 files+37−50/4 viewed
+1−0
9393 None => return Ok(Some(error(id, -32602, "Unknown tool."))),
9494 },
9595 };
96+ let arguments = &crate::tools::with_defaults(op, arguments);
9697 let outcome = crate::audit::run(op, services, viewer, arguments).await?;
9798 // A failed operation is a tool result the model can read and
9899 // act on, not a protocol error.
+32−1
8585 a("contents", Op::GetContents, "A file's text, or a directory's entries and README, at a branch, tag or commit"),
8686 a("tarball", Op::Archives(ArchiveOp::DownloadTarball), "A 10-minute link to a branch, tag or commit as a .tar.gz"),
8787 a("zipball", Op::Archives(ArchiveOp::DownloadZipball), "A 10-minute link to a branch, tag or commit as a .zip"),
88− a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL"),
88+ a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL; private unless private is false"),
8989 a("update", Op::UpdateRepo, "Change description (its project's), website, topics, default branch, protection, forking"),
9090 a("get_settings", Op::GetRepoSettings, "How pull requests merge, and the default branch's protection as its rules stack"),
9191 a("update_settings", Op::UpdateRepoSettings, "Change how pull requests merge and the default branch protection ruleset"),
848848 }
849849 }
850850
851+/// A call's arguments with the MCP server's own defaults, where they differ
852+/// from the REST API's: a repository an agent creates is private unless it
853+/// says otherwise (`private: false`, or `visibility: "public"`), so nothing
854+/// it starts is public by accident. REST keeps its default, public.
855+pub fn with_defaults(op: Op, arguments: &Value) -> Value {
856+ let mut arguments = arguments.clone();
857+ if op == Op::CreateRepo
858+ && let Some(fields) = arguments.as_object_mut()
859+ && !fields.get("private").is_some_and(Value::is_boolean)
860+ {
861+ let public = fields.get("visibility").and_then(Value::as_str).is_some_and(|v| v.eq_ignore_ascii_case("public"));
862+ fields.insert("private".to_owned(), Value::Bool(!public));
863+ }
864+ arguments
865+}
866+
851867 /// What a call to a tool runs: the operation its action names, or why not.
852868 pub fn resolve(tool: &Tool, arguments: &Value) -> Result<Op, String> {
853869 let names = || {
895911 }
896912
897913 #[test]
914+ fn a_repository_created_over_mcp_is_private_unless_it_says_public() {
915+ let made = |arguments: Value| with_defaults(Op::CreateRepo, &arguments)["private"].clone();
916+ assert_eq!(made(json!({ "name": "web" })), json!(true));
917+ assert_eq!(made(json!({ "name": "web", "private": null })), json!(true));
918+ assert_eq!(made(json!({ "name": "web", "private": true })), json!(true));
919+ assert_eq!(made(json!({ "name": "web", "private": false })), json!(false));
920+ assert_eq!(made(json!({ "name": "web", "visibility": "public" })), json!(false));
921+ assert_eq!(made(json!({ "name": "web", "visibility": "private" })), json!(true));
922+ // Other operations' arguments are left as given.
923+ assert_eq!(with_defaults(Op::UpdateRepo, &json!({ "repo": "acme/web" })), json!({ "repo": "acme/web" }));
924+ let repository = Tool::by_name("repository").unwrap();
925+ assert!(repository.action("create").unwrap().summary.contains("private unless private is false"));
926+ }
927+
928+ #[test]
898929 fn every_operation_is_exactly_one_action_of_one_tool() {
899930 for op in Op::ALL {
900931 let count = TOOLS
+3−3
7979 its own name.
8080
8181 Through MCP, the same is the `repository` tool's `create` action with
82− `workspace`, `name`, `description` and `private: true`. There a
83− repository is public unless `private` is true. The agent then pushes
84− with git.
82+ `workspace`, `name` and `description`. There too a repository is
83+ private unless `private: false` is given. The agent then pushes with
84+ git.
8585
8686 <Aside title="Record the session from here">
8787 The plugin records a session only when it starts in a working copy
+1−1
259259 | [`contents`](/reference/api/repositories/get-contents/) | What it holds at `path` (the root when left out) at `ref` (the default branch when left out): a file's `text` (null for a binary one; at most 200,000 characters), or a directory's `entries` and its README. | `repo` | `code:read` |
260260 | [`tarball`](/reference/api/repositories/download-tarball-archive/) | A link to a branch, tag or commit (`ref`, the default branch when left out) as a `.tar.gz` that needs no token, good for 10 minutes, as `url`, with the `commit` it is of. | `repo` | `code:read` |
261261 | [`zipball`](/reference/api/repositories/download-zipball-archive/) | The same as a `.zip`. | `repo` | `code:read` |
262−| [`create`](/reference/api/repositories/create-repo/) | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `description` becomes its [project's](/guides/projects/), as a repository has none of its own. `workspace` may be left out if you belong to exactly one. | `name` | `repo:create`, which `repo:write` includes and the [Agent preset](/guides/authentication/#presets) gives |
262+| [`create`](/reference/api/repositories/create-repo/) | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `description` becomes its [project's](/guides/projects/), as a repository has none of its own. `workspace` may be left out if you belong to exactly one. It is private unless `private` is false (or `visibility` is `public`); the [REST API](/reference/api/repositories/create-repo/) makes one public unless `private` is true. | `name` | `repo:create`, which `repo:write` includes and the [Agent preset](/guides/authentication/#presets) gives |
263263 | [`update`](/reference/api/repositories/update-repo/) | Change its `website`, `topics` and `default_branch`, whether its default branch is `protected`, whether it is `private`, and whether a private repository may be forked (`allow_forking`). Maintain role; `protected`, `private`, `allow_forking` and `default_branch` need Admin, and `private` the [member privileges](/guides/workspaces/#member-privileges) unless you are an owner. | `repo` | `repo:write` |
264264 | [`get_settings`](/reference/api/repositories/get-repo-settings/) | How it handles pull requests: how they merge, and the default branch's required checks, approvals, bypassing checks, being up to date and merge queue as its [rulesets](/guides/rules/) stack. | `repo` | `repo:read` |
265265 | [`update_settings`](/reference/api/repositories/update-repo-settings/) | Change those settings. Only the fields given change; `required_checks` replaces the whole list. Maintain role. | `repo` | `repo:write` |

Comments

Sign in to comment.