// Package localtools holds MCP tools that must never be reachable from the // shared HTTP gitea-mcp server (RegisterAll, cmd/gitea-mcp). Each tool here // carries a capability too dangerous to expose network-side, even behind // OIDC — mint_service_account_token resets whatever password its target // account has. Only cmd/gitea-admin-mcp, a separate local-only stdio binary, // imports this package. package localtools import ( "context" "encoding/json" "fmt" "strings" "git.d-ma.be/mathias/gitea-mcp/internal/adminbootstrap" "git.d-ma.be/mathias/gitea-mcp/internal/registry" ) // bootstrapper is the one method this tool needs — narrow so tests inject a // fake instead of a real adminbootstrap.Bootstrapper. type bootstrapper interface { Run(ctx context.Context, req adminbootstrap.Request) (adminbootstrap.Result, error) } type MintServiceAccountToken struct { b bootstrapper } func NewMintServiceAccountToken(b bootstrapper) *MintServiceAccountToken { return &MintServiceAccountToken{b: b} } func (t *MintServiceAccountToken) Descriptor() registry.ToolDescriptor { return registry.ToolDescriptor{ Name: "mint_service_account_token", Description: "Create (or reset the password of) a dedicated Gitea " + "service/bot account, mint it a scoped access token, optionally " + "grant it read-only collaborator access on named repos, and store " + "the token via 1Password. Requires confirm_service_account=true: " + "this resets whatever password the target account currently has, " + "which is only safe for a dedicated bot account, never a real " + "person's login.", InputSchema: json.RawMessage(`{ "type":"object", "properties":{ "username":{"type":"string"}, "email":{"type":"string"}, "full_name":{"type":"string"}, "scopes":{"type":"array","items":{"type":"string"}}, "token_name":{"type":"string"}, "secret_name":{"type":"string","description":"name to store the minted token under"}, "grant_read_repos":{"type":"array","items":{"type":"string"},"description":"owner/repo strings"}, "confirm_service_account":{"type":"boolean"} }, "required":["username","email","scopes","token_name","secret_name","confirm_service_account"] }`), } } type mintArgs struct { Username string `json:"username"` Email string `json:"email"` FullName string `json:"full_name"` Scopes []string `json:"scopes"` TokenName string `json:"token_name"` SecretName string `json:"secret_name"` GrantReadRepos []string `json:"grant_read_repos"` ConfirmServiceAccount bool `json:"confirm_service_account"` } func (t *MintServiceAccountToken) Call(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) { var args mintArgs if err := json.Unmarshal(raw, &args); err != nil { return nil, fmt.Errorf("parse arguments: %w", err) } repos := make([]adminbootstrap.RepoRef, 0, len(args.GrantReadRepos)) for _, r := range args.GrantReadRepos { owner, name, ok := strings.Cut(r, "/") if !ok || owner == "" || name == "" { return nil, fmt.Errorf("grant_read_repos entry %q must be owner/repo", r) } repos = append(repos, adminbootstrap.RepoRef{Owner: owner, Name: name}) } result, err := t.b.Run(ctx, adminbootstrap.Request{ Username: args.Username, Email: args.Email, FullName: args.FullName, Scopes: args.Scopes, TokenName: args.TokenName, SecretName: args.SecretName, GrantReadRepos: repos, ConfirmServiceAccount: args.ConfirmServiceAccount, }) if err != nil { return nil, err } return json.Marshal(result) }