Skip to content

Commit 5602356

Browse files
Optimize get_file_contents: eliminate raw API call
This optimization reduces the API calls for get_file_contents from 2 to 1 by: 1. Using the Content field from GitHub's Contents API directly instead of fetching content separately via the raw API 2. Inferring Content-Type locally using file extension mapping and mimetype library for magic byte detection Benefits: - ~50% latency reduction (one round-trip instead of two) - Lower rate limit consumption - Simpler error handling The mimetype inference uses a hybrid approach: - Custom extension map for accurate code file type detection (handles edge cases like .ts which stdlib maps incorrectly to Qt Linguist) - Magic byte detection via gabriel-vasile/mimetype library for binary vs text detection and shebang script identification Also adds raw_content parameter for clients that don't support embedded resources (returns plain text instead of resource format).
1 parent f663214 commit 5602356

File tree

9 files changed

+780
-127
lines changed

9 files changed

+780
-127
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,8 @@ Possible options:
10931093
- **get_file_contents** - Get file or directory contents
10941094
- `owner`: Repository owner (username or organization) (string, required)
10951095
- `path`: Path to file/directory (string, optional)
1096-
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
1096+
- `raw_content`: If true, returns file content as plain text (for text files) or base64-encoded (for binary files) instead of embedded resource format. Useful for clients that don't support embedded resources. (boolean, optional)
1097+
- `ref`: Accepts optional git refs such as refs/tags/{tag}, refs/heads/{branch} or refs/pull/{pr_number}/head (string, optional)
10971098
- `repo`: Repository name (string, required)
10981099
- `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)
10991100

docs/remote-server.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
1919
<!-- START AUTOMATED TOOLSETS -->
2020
| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |
2121
|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
22-
| Default | ["Default" toolset](../README.md#default-toolset) | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |
22+
| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |
2323
| Actions | GitHub Actions workflows and CI/CD operations | https://api.githubcopilot.com/mcp/x/actions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/actions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%2Freadonly%22%7D) |
2424
| Code Security | Code security related tools, such as GitHub Code Scanning | https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D) |
2525
| Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) |

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616

1717
require (
1818
github.com/aymerick/douceur v0.2.0 // indirect
19+
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
1920
github.com/go-openapi/jsonpointer v0.19.5 // indirect
2021
github.com/go-openapi/swag v0.21.1 // indirect
2122
github.com/google/go-github/v71 v71.0.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
1010
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
1111
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
1212
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
13+
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
14+
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
1315
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
1416
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
1517
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=

pkg/github/__toolsnaps__/get_file_contents.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
"description": "Path to file/directory",
2121
"default": "/"
2222
},
23+
"raw_content": {
24+
"type": "boolean",
25+
"description": "If true, returns file content as plain text (for text files) or base64-encoded (for binary files) instead of embedded resource format. Useful for clients that don't support embedded resources."
26+
},
2327
"ref": {
2428
"type": "string",
25-
"description": "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`"
29+
"description": "Accepts optional git refs such as refs/tags/{tag}, refs/heads/{branch} or refs/pull/{pr_number}/head"
2630
},
2731
"repo": {
2832
"type": "string",

pkg/github/mimetype.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package github
2+
3+
import (
4+
"path"
5+
"strings"
6+
7+
"github.com/gabriel-vasile/mimetype"
8+
)
9+
10+
// codeExtensionMimeTypes maps common code file extensions to MIME types.
11+
// This is needed because Go's stdlib mime.TypeByExtension has gaps and wrong mappings
12+
// for many code-related extensions (e.g., .ts returns Qt Linguist, .tsx returns nothing).
13+
var codeExtensionMimeTypes = map[string]string{
14+
// JavaScript/TypeScript
15+
".ts": "text/typescript",
16+
".tsx": "text/typescript-jsx",
17+
".mts": "text/typescript",
18+
".cts": "text/typescript",
19+
".js": "text/javascript",
20+
".jsx": "text/javascript-jsx",
21+
".mjs": "text/javascript",
22+
".cjs": "text/javascript",
23+
".vue": "text/x-vue",
24+
".svelte": "text/x-svelte",
25+
26+
// Go
27+
".go": "text/x-go",
28+
".mod": "text/x-go-mod",
29+
".sum": "text/x-go-sum",
30+
".work": "text/x-go-work",
31+
32+
// Rust
33+
".rs": "text/x-rust",
34+
".toml": "text/x-toml",
35+
36+
// Python
37+
".py": "text/x-python",
38+
".pyi": "text/x-python",
39+
".pyx": "text/x-cython",
40+
".pxd": "text/x-cython",
41+
42+
// Ruby
43+
".rb": "text/x-ruby",
44+
".rake": "text/x-ruby",
45+
".gemspec": "text/x-ruby",
46+
".erb": "text/x-erb",
47+
48+
// Java/Kotlin/Scala
49+
".java": "text/x-java-source",
50+
".kt": "text/x-kotlin",
51+
".kts": "text/x-kotlin",
52+
".scala": "text/x-scala",
53+
".groovy": "text/x-groovy",
54+
55+
// C family
56+
".c": "text/x-c",
57+
".h": "text/x-c",
58+
".cpp": "text/x-c++",
59+
".cc": "text/x-c++",
60+
".cxx": "text/x-c++",
61+
".hpp": "text/x-c++",
62+
".hh": "text/x-c++",
63+
".hxx": "text/x-c++",
64+
".m": "text/x-objective-c",
65+
".mm": "text/x-objective-c++",
66+
67+
// C#/F#
68+
".cs": "text/x-csharp",
69+
".fs": "text/x-fsharp",
70+
71+
// Swift
72+
".swift": "text/x-swift",
73+
74+
// PHP
75+
".php": "text/x-php",
76+
".phtml": "text/x-php",
77+
78+
// Shell scripts
79+
".sh": "text/x-shellscript",
80+
".bash": "text/x-shellscript",
81+
".zsh": "text/x-shellscript",
82+
".fish": "text/x-shellscript",
83+
84+
// Config/Data files
85+
".json": "application/json",
86+
".yml": "text/yaml",
87+
".yaml": "text/yaml",
88+
".xml": "text/xml",
89+
".ini": "text/x-ini",
90+
".cfg": "text/x-ini",
91+
".conf": "text/plain",
92+
".env": "text/plain",
93+
94+
// Markup/Documentation
95+
".md": "text/markdown",
96+
".markdown": "text/markdown",
97+
".rst": "text/x-rst",
98+
".adoc": "text/asciidoc",
99+
".tex": "text/x-tex",
100+
101+
// Web
102+
".html": "text/html",
103+
".htm": "text/html",
104+
".css": "text/css",
105+
".scss": "text/x-scss",
106+
".sass": "text/x-sass",
107+
".less": "text/x-less",
108+
109+
// SQL
110+
".sql": "text/x-sql",
111+
112+
// Other languages
113+
".lua": "text/x-lua",
114+
".r": "text/x-r",
115+
".R": "text/x-r",
116+
".jl": "text/x-julia",
117+
".ex": "text/x-elixir",
118+
".exs": "text/x-elixir",
119+
".erl": "text/x-erlang",
120+
".hrl": "text/x-erlang",
121+
".clj": "text/x-clojure",
122+
".cljs": "text/x-clojure",
123+
".cljc": "text/x-clojure",
124+
".hs": "text/x-haskell",
125+
".lhs": "text/x-haskell",
126+
".ml": "text/x-ocaml",
127+
".mli": "text/x-ocaml",
128+
".nim": "text/x-nim",
129+
".dart": "text/x-dart",
130+
".v": "text/x-v",
131+
".zig": "text/x-zig",
132+
133+
// Build/Config files
134+
".dockerfile": "text/x-dockerfile",
135+
".makefile": "text/x-makefile",
136+
137+
// Special files
138+
".gitignore": "text/plain",
139+
".dockerignore": "text/plain",
140+
".editorconfig": "text/plain",
141+
}
142+
143+
// isTextMIME returns true if the MIME type indicates text content.
144+
func isTextMIME(mimeType string) bool {
145+
if strings.HasPrefix(mimeType, "text/") {
146+
return true
147+
}
148+
// Common application/* types that are actually text
149+
textApplicationTypes := []string{
150+
"application/json",
151+
"application/xml",
152+
"application/javascript",
153+
"application/typescript",
154+
"application/x-sh",
155+
"application/x-shellscript",
156+
}
157+
for _, t := range textApplicationTypes {
158+
if mimeType == t {
159+
return true
160+
}
161+
}
162+
// Types with +json, +xml suffix are text
163+
if strings.HasSuffix(mimeType, "+json") || strings.HasSuffix(mimeType, "+xml") {
164+
return true
165+
}
166+
return false
167+
}
168+
169+
// inferContentType infers the content type from file extension and optionally content.
170+
// Returns the inferred MIME type and whether it's a text file.
171+
func inferContentType(filePath string, content []byte) (mimeType string, isText bool) {
172+
ext := strings.ToLower(path.Ext(filePath))
173+
174+
// Handle special filenames (Dockerfile, Makefile, etc.)
175+
baseName := strings.ToLower(path.Base(filePath))
176+
if ext == "" {
177+
switch baseName {
178+
case "dockerfile":
179+
return "text/x-dockerfile", true
180+
case "makefile", "gnumakefile":
181+
return "text/x-makefile", true
182+
case "rakefile":
183+
return "text/x-ruby", true
184+
case "gemfile":
185+
return "text/x-ruby", true
186+
case "vagrantfile":
187+
return "text/x-ruby", true
188+
case "procfile":
189+
return "text/plain", true
190+
case "readme", "license", "authors", "changelog", "contributing":
191+
return "text/plain", true
192+
}
193+
}
194+
195+
// Check our extension map first (more accurate for code files)
196+
if mtype, ok := codeExtensionMimeTypes[ext]; ok {
197+
return mtype, isTextMIME(mtype)
198+
}
199+
200+
// If we have content, use mimetype library for accurate detection
201+
if len(content) > 0 {
202+
mtype := mimetype.Detect(content)
203+
return mtype.String(), isTextMIME(mtype.String())
204+
}
205+
206+
// Fall back to extension-only detection using mimetype library
207+
return inferContentTypeFromExtension(ext)
208+
}
209+
210+
// inferContentTypeFromExtension infers MIME type from extension only.
211+
// Used when we don't have file content available.
212+
func inferContentTypeFromExtension(ext string) (mimeType string, isText bool) {
213+
ext = strings.ToLower(ext)
214+
215+
// Check our extension map first
216+
if mtype, ok := codeExtensionMimeTypes[ext]; ok {
217+
return mtype, isTextMIME(mtype)
218+
}
219+
220+
// Use mimetype library for other extensions
221+
// mimetype.Lookup returns the MIME type for a given extension
222+
mtype := mimetype.Lookup(ext)
223+
if mtype != nil {
224+
return mtype.String(), isTextMIME(mtype.String())
225+
}
226+
227+
// Default to binary for unknown types
228+
return "application/octet-stream", false
229+
}

0 commit comments

Comments
 (0)