@@ -10,6 +10,7 @@ import (
1010 "path/filepath"
1111 "strconv"
1212 "strings"
13+ "unicode/utf8"
1314
1415 "github.com/github/github-mcp-server/v2/pkg/inventory"
1516 "github.com/github/github-mcp-server/v2/pkg/octicons"
@@ -247,53 +248,102 @@ func RepositoryResourceContentsHandler(resourceURITemplate *uritemplate.Template
247248 }
248249}
249250
250- // expandRepoResourceURI builds a resource URI using the appropriate URI template
251- // based on the provided parameters (sha, ref, or default).
251+ func escapeRepoResourceURIComponent (value string ) (string , error ) {
252+ if ! utf8 .ValidString (value ) {
253+ return "" , errors .New ("invalid UTF-8 in repository resource URI" )
254+ }
255+
256+ const hex = "0123456789ABCDEF"
257+ var escaped strings.Builder
258+ escaped .Grow (len (value ))
259+ for i := range len (value ) {
260+ b := value [i ]
261+ if (b >= 'a' && b <= 'z' ) || (b >= 'A' && b <= 'Z' ) ||
262+ (b >= '0' && b <= '9' ) || b == '-' || b == '.' || b == '_' || b == '~' {
263+ escaped .WriteByte (b )
264+ continue
265+ }
266+ escaped .WriteByte ('%' )
267+ escaped .WriteByte (hex [b >> 4 ])
268+ escaped .WriteByte (hex [b & 0x0f ])
269+ }
270+ return escaped .String (), nil
271+ }
272+
252273func expandRepoResourceURI (owner , repo , sha , ref string , pathParts []string ) (string , error ) {
253- baseValues := uritemplate.Values {
254- "owner" : uritemplate .String (owner ),
255- "repo" : uritemplate .String (repo ),
256- "path" : uritemplate .List (pathParts ... ),
274+ owner , err := escapeRepoResourceURIComponent (owner )
275+ if err != nil {
276+ return "" , err
277+ }
278+ repo , err = escapeRepoResourceURIComponent (repo )
279+ if err != nil {
280+ return "" , err
281+ }
282+
283+ path := make ([]string , len (pathParts ))
284+ for i , part := range pathParts {
285+ path [i ], err = escapeRepoResourceURIComponent (part )
286+ if err != nil {
287+ return "" , err
288+ }
289+ }
290+
291+ base := "repo://" + owner + "/" + repo
292+ withPath := func (uri string ) string {
293+ uri += "/contents"
294+ if len (path ) > 0 {
295+ uri += "/" + strings .Join (path , "/" )
296+ }
297+ return uri
257298 }
258299
259300 switch {
260301 case sha != "" :
261- baseValues ["sha" ] = uritemplate .String (sha )
262- return repositoryResourceCommitContentURITemplate .Expand (baseValues )
302+ sha , err = escapeRepoResourceURIComponent (sha )
303+ if err != nil {
304+ return "" , err
305+ }
306+ return withPath (base + "/sha/" + sha ), nil
263307
264308 case ref != "" :
265- // Parse ref to determine which template to use
266309 switch {
267310 case strings .HasPrefix (ref , "refs/heads/" ):
268- branch := strings .TrimPrefix (ref , "refs/heads/" )
269- baseValues ["branch" ] = uritemplate .String (branch )
270- return repositoryResourceBranchContentURITemplate .Expand (baseValues )
311+ branch , err := escapeRepoResourceURIComponent (strings .TrimPrefix (ref , "refs/heads/" ))
312+ if err != nil {
313+ return "" , err
314+ }
315+ return withPath (base + "/refs/heads/" + branch ), nil
271316
272317 case strings .HasPrefix (ref , "refs/tags/" ):
273- tag := strings .TrimPrefix (ref , "refs/tags/" )
274- baseValues ["tag" ] = uritemplate .String (tag )
275- return repositoryResourceTagContentURITemplate .Expand (baseValues )
318+ tag , err := escapeRepoResourceURIComponent (strings .TrimPrefix (ref , "refs/tags/" ))
319+ if err != nil {
320+ return "" , err
321+ }
322+ return withPath (base + "/refs/tags/" + tag ), nil
276323
277324 case strings .HasPrefix (ref , "refs/pull/" ) && strings .HasSuffix (ref , "/head" ):
278- // Extract PR number from "refs/pull/{number} /head"
279- prPart := strings . TrimPrefix ( ref , "refs/pull/" )
280- prNumber := strings . TrimSuffix ( prPart , "/head" )
281- baseValues [ "prNumber" ] = uritemplate . String ( prNumber )
282- return repositoryResourcePrContentURITemplate . Expand ( baseValues )
325+ prNumber , err := escapeRepoResourceURIComponent ( strings . TrimSuffix ( strings . TrimPrefix ( ref , "refs/pull/" ), " /head"))
326+ if err != nil {
327+ return "" , err
328+ }
329+ return withPath ( base + "/refs/pull/" + prNumber + "/head" ), nil
283330
284331 case looksLikeSHA (ref ):
285- // ref is actually a SHA (e.g., from resolveGitReference)
286- baseValues ["sha" ] = uritemplate .String (ref )
287- return repositoryResourceCommitContentURITemplate .Expand (baseValues )
332+ sha , err := escapeRepoResourceURIComponent (ref )
333+ if err != nil {
334+ return "" , err
335+ }
336+ return withPath (base + "/sha/" + sha ), nil
288337
289338 default :
290- // For other refs (like a branch name without refs/heads/ prefix),
291- // treat it as a branch
292- baseValues ["branch" ] = uritemplate .String (ref )
293- return repositoryResourceBranchContentURITemplate .Expand (baseValues )
339+ branch , err := escapeRepoResourceURIComponent (ref )
340+ if err != nil {
341+ return "" , err
342+ }
343+ return withPath (base + "/refs/heads/" + branch ), nil
294344 }
295345
296346 default :
297- return repositoryResourceContentURITemplate . Expand ( baseValues )
347+ return withPath ( base ), nil
298348 }
299349}
0 commit comments