Skip to content

Commit 34e0eef

Browse files
committed
fix(resources): return binary application/* files from repo:// resources as blobs
RepositoryResourceContentsHandler returned every application/* response as text. raw.githubusercontent.com serves binary files such as PDFs and archives as application/octet-stream, so reading them through a repo:// resource put the raw bytes into the text field, where invalid UTF-8 is replaced during JSON encoding and the content cannot be recovered. Return text only for text/* and for text-based application types (application/json, application/xml, application/javascript, and application/*+json or application/*+xml), matching how get_file_contents classifies content, and return everything else as a blob. Other top-level types keep their previous handling (for example image/svg+xml stays a blob), and a Content-Type with a malformed parameter is still classified by its media type. Assisted-by: Claude:claude-opus-5-5
1 parent f385262 commit 34e0eef

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

‎pkg/github/repository_resource.go‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func RepositoryResourceContentsHandler(resourceURITemplate *uritemplate.Template
213213
}
214214

215215
switch {
216-
case strings.HasPrefix(mimeType, "text"), strings.HasPrefix(mimeType, "application"):
216+
case isTextMIMEType(mimeType):
217217
return &mcp.ReadResourceResult{
218218
Contents: []*mcp.ResourceContents{
219219
{
@@ -248,6 +248,27 @@ func RepositoryResourceContentsHandler(resourceURITemplate *uritemplate.Template
248248
}
249249
}
250250

251+
// isTextMIMEType reports whether content of the given MIME type can be returned
252+
// as text. Other application/* types, such as application/octet-stream which
253+
// raw.githubusercontent.com uses for binary files, must be returned as blobs.
254+
func isTextMIMEType(mimeType string) bool {
255+
mediaType, _, err := mime.ParseMediaType(mimeType)
256+
if err != nil && !errors.Is(err, mime.ErrInvalidMediaParameter) {
257+
return false
258+
}
259+
if strings.HasPrefix(mediaType, "text/") {
260+
return true
261+
}
262+
if !strings.HasPrefix(mediaType, "application/") {
263+
return false
264+
}
265+
return mediaType == "application/json" ||
266+
mediaType == "application/xml" ||
267+
mediaType == "application/javascript" ||
268+
strings.HasSuffix(mediaType, "+json") ||
269+
strings.HasSuffix(mediaType, "+xml")
270+
}
271+
251272
func escapeRepoResourceURIComponent(value string) (string, error) {
252273
if !utf8.ValidString(value) {
253274
return "", errors.New("invalid UTF-8 in repository resource URI")

‎pkg/github/repository_resource_test.go‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,58 @@ func Test_repositoryResourceContentsHandler_NetworkError(t *testing.T) {
416416
require.Nil(t, resp)
417417
require.ErrorContains(t, err, "failed to get raw content")
418418
}
419+
420+
// Test_repositoryResourceContentsHandler_ApplicationTypes verifies that binary files
421+
// served with an application/* Content-Type (raw.githubusercontent.com serves PDFs,
422+
// archives and other binaries as application/octet-stream) are returned as blobs
423+
// instead of being coerced into lossy text, while text-based application types
424+
// are still returned as text.
425+
func Test_repositoryResourceContentsHandler_ApplicationTypes(t *testing.T) {
426+
base, _ := url.Parse("https://raw.example.com/")
427+
binary := []byte("%PDF-1.4\n%\xe2\xe3\xcf\xd3\n\x00\x01\xff\xfe")
428+
text := []byte(`{"name": "test"}`)
429+
430+
tests := []struct {
431+
contentType string
432+
content []byte
433+
wantBlob bool
434+
}{
435+
{contentType: "application/octet-stream", content: binary, wantBlob: true},
436+
{contentType: "application/pdf", content: binary, wantBlob: true},
437+
{contentType: "application/zip", content: binary, wantBlob: true},
438+
{contentType: "application/json; charset=utf-8", content: text},
439+
{contentType: "application/xml", content: text},
440+
{contentType: "application/vnd.api+json", content: text},
441+
{contentType: "text/plain; charset", content: text},
442+
{contentType: "image/svg+xml", content: []byte(`<svg xmlns="http://www.w3.org/2000/svg"/>`), wantBlob: true},
443+
}
444+
445+
for _, tc := range tests {
446+
t.Run(tc.contentType, func(t *testing.T) {
447+
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
448+
GetRawReposContentsByOwnerByRepoByPath: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
449+
w.Header().Set("Content-Type", tc.contentType)
450+
_, err := w.Write(tc.content)
451+
require.NoError(t, err)
452+
}),
453+
}))
454+
rawClient, err := raw.NewClient(client, base)
455+
require.NoError(t, err)
456+
ctx := ContextWithDeps(context.Background(), BaseDeps{Client: client, RawClient: rawClient})
457+
458+
resp, err := RepositoryResourceContentsHandler(repositoryResourceContentURITemplate)(ctx, &mcp.ReadResourceRequest{
459+
Params: &mcp.ReadResourceParams{URI: "repo://owner/repo/contents/docs/file.bin"},
460+
})
461+
require.NoError(t, err)
462+
require.Len(t, resp.Contents, 1)
463+
require.Equal(t, tc.contentType, resp.Contents[0].MIMEType)
464+
if tc.wantBlob {
465+
require.Empty(t, resp.Contents[0].Text)
466+
require.Equal(t, tc.content, resp.Contents[0].Blob)
467+
} else {
468+
require.Empty(t, resp.Contents[0].Blob)
469+
require.Equal(t, string(tc.content), resp.Contents[0].Text)
470+
}
471+
})
472+
}
473+
}

0 commit comments

Comments
 (0)