6 Commits

Author SHA1 Message Date
4f0f65e26a Merge pull request 'fix: add OAuth discovery endpoints for claude.ai handshake' (#3) from fix/oauth-discovery-endpoints into main
All checks were successful
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 3s
Reviewed-on: http://gitea.d-ma.be/mathias/gitea-mcp/pulls/3
2026-05-06 15:20:58 +00:00
Mathias Bergqvist
9cbb564cd9 fix: add OAuth discovery endpoints for claude.ai handshake
All checks were successful
CD / Lint / Test / Vet (pull_request) Successful in 5s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
Implements RFC 9728 protected resource metadata and HEAD probe so
claude.ai can complete its pre-handshake discovery without hitting 404.

- GET /.well-known/oauth-protected-resource → 200 {"authorization_servers":[]}
- GET /.well-known/oauth-authorization-server → 404 (no auth server)
- HEAD /mcp → 200 + MCP-Protocol-Version: 2025-06-18 header

Closes #2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:19:14 +02:00
47e631da23 Merge pull request 'fix(file_write_branch): support file creation by routing POST/PUT on sha' (#1) from fix/file-write-branch-create into main
All checks were successful
CD / Lint / Test / Vet (push) Successful in 6s
CD / Build & Import (push) Successful in 12s
CD / Deploy via GitOps (push) Successful in 3s
Reviewed-on: http://gitea.d-ma.be/mathias/gitea-mcp/pulls/1
2026-05-06 14:44:38 +00:00
d35ff9781c test(file_write_branch): assert branch and commit_sha on PUT path for parity
All checks were successful
CD / Lint / Test / Vet (pull_request) Successful in 5s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
2026-05-06 14:35:20 +00:00
052827320a test(file_write_branch): cover POST-on-create and PUT-on-update routing
All checks were successful
CD / Lint / Test / Vet (pull_request) Successful in 6s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
2026-05-06 14:05:23 +00:00
c85197ea5e fix(files): route UpsertFile to POST when sha is empty so new files can be created 2026-05-06 14:04:36 +00:00
5 changed files with 71 additions and 3 deletions

View File

@@ -54,6 +54,18 @@ func main() {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/.well-known/oauth-protected-resource", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"authorization_servers":[]}`))
})
mux.HandleFunc("/.well-known/oauth-authorization-server", func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
})
addr := ":" + cfg.Port
logger.Info("gitea-mcp starting", "addr", addr, "version", "0.1.0")

View File

@@ -92,13 +92,24 @@ type FileWriteResult struct {
} `json:"commit"`
}
// UpsertFile creates a file when args.Sha is empty (POST) or updates an existing
// file when args.Sha is set (PUT). Gitea routes both operations by HTTP method on
// the same /contents/{path} URL, and rejects PUT without a sha.
func (c *Client) UpsertFile(ctx context.Context, owner, repo, path string, args UpsertFileArgs) (*FileWriteResult, error) {
p := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, path)
payload, err := json.Marshal(args)
if err != nil {
return nil, err
}
body, status, err := c.PutJSON(ctx, p, payload)
var (
body []byte
status int
)
if args.Sha == "" {
body, status, err = c.PostJSON(ctx, p, payload)
} else {
body, status, err = c.PutJSON(ctx, p, payload)
}
if err != nil {
return nil, err
}

View File

@@ -31,6 +31,9 @@ func NewServer(opts ServerOptions) *Server {
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodHead:
w.Header().Set("MCP-Protocol-Version", ProtocolVersion)
w.WriteHeader(http.StatusOK)
case http.MethodGet:
s.handleGET(w, r)
case http.MethodPost:

View File

@@ -118,6 +118,15 @@ func TestPostBodyTooLarge(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestHEADReturnsMCPProtocolVersionHeader(t *testing.T) {
srv := newServer(t)
req := httptest.NewRequest(http.MethodHead, "/mcp", nil)
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, mcp.ProtocolVersion, rr.Header().Get("MCP-Protocol-Version"))
}
func TestToolsCallToolNotFound(t *testing.T) {
srv := newServer(t)
// Initialize to get a session ID.

View File

@@ -39,9 +39,9 @@ func TestFileWriteBranchCreatesBranchAndFile(t *testing.T) {
_, _ = w.Write([]byte(createBranchResp))
})
// Upsert file → 201
// New file (no sha) → POST to /contents/{path}
mux.HandleFunc("/api/v1/repos/owner/myrepo/contents/doc.md", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPut, r.Method)
require.Equal(t, http.MethodPost, r.Method)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(upsertFileResp))
})
@@ -64,6 +64,39 @@ func TestFileWriteBranchCreatesBranchAndFile(t *testing.T) {
assert.Equal(t, "cmt1", result["commit_sha"])
}
func TestFileWriteBranchUsesPutWhenShaProvided(t *testing.T) {
mux := http.NewServeMux()
// Branch exists
mux.HandleFunc("/api/v1/repos/owner/myrepo/branches/feat/existing", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(branchCheckExistsResp))
})
// Existing file (sha provided) → PUT
mux.HandleFunc("/api/v1/repos/owner/myrepo/contents/doc.md", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPut, r.Method)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(upsertFileResp))
})
srv := httptest.NewServer(mux)
defer srv.Close()
tool := tools.NewFileWriteBranch(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"owner"}))
out, err := tool.Call(context.Background(), json.RawMessage(`{
"owner":"owner","name":"myrepo","path":"doc.md",
"content":"hello","branch":"feat/existing",
"sha":"oldsha","message":"update doc.md"
}`))
require.NoError(t, err)
var result map[string]any
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, "feat/existing", result["branch"])
assert.Equal(t, "cmt1", result["commit_sha"])
}
func TestFileWriteBranchUsesDefaultBaseWhenBaseEmpty(t *testing.T) {
var createBody []byte
mux := http.NewServeMux()