Skip to content

Commit fe4e3c5

Browse files
committed
add more evals for copilot cli tools
1 parent 0c7a33b commit fe4e3c5

12 files changed

Lines changed: 615 additions & 9 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ The following sets of tools are available:
980980
- **search_orgs** - Search organizations
981981
- **Required OAuth Scopes**: `read:org`
982982
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `write:org`
983+
- `fields`: Subset of fields to return for each organization. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields, e.g. just 'login'. (string[], optional)
983984
- `order`: Sort order (string, optional)
984985
- `page`: Page number for pagination (min 1) (number, optional)
985986
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
@@ -1212,6 +1213,7 @@ The following sets of tools are available:
12121213

12131214
- **get_file_contents** - Get file or directory contents
12141215
- **Required OAuth Scopes**: `repo`
1216+
- `fields`: Subset of fields to return for each entry when the path is a directory. If omitted, all fields are returned. Ignored when the path is a single file. Use this to reduce response size when listing directories and you only need specific fields, e.g. just 'name' and 'type'. (string[], optional)
12151217
- `owner`: Repository owner (username or organization) (string, required)
12161218
- `path`: Path to file/directory (string, optional)
12171219
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
@@ -1282,6 +1284,7 @@ The following sets of tools are available:
12821284

12831285
- **search_code** - Search code
12841286
- **Required OAuth Scopes**: `repo`
1287+
- `fields`: Subset of fields to return for each code search result. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'repository' in particular drops the largest per-result object. (string[], optional)
12851288
- `order`: Sort order for results (string, optional)
12861289
- `page`: Page number for pagination (min 1) (number, optional)
12871290
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
@@ -1394,6 +1397,7 @@ The following sets of tools are available:
13941397

13951398
- **search_users** - Search users
13961399
- **Required OAuth Scopes**: `repo`
1400+
- `fields`: Subset of fields to return for each user. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields, e.g. just 'login'. (string[], optional)
13971401
- `order`: Sort order (string, optional)
13981402
- `page`: Page number for pagination (min 1) (number, optional)
13991403
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)

evals/schema_fields_eval.py

Lines changed: 263 additions & 7 deletions
Large diffs are not rendered by default.

evals/tasks_cli_tools.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Focused task set for the Copilot CLI-kept tools that just gained a `fields`
2+
# param: search_code, get_file_contents (repos toolset) and search_users (users).
3+
# Run with: --toolsets repos,users --tasks-file tasks_cli_tools.txt
4+
# Format: tag<TAB>task text. {repo}/{org} are substituted from --repo/--org.
5+
# Tags (per-type breakdown only, never sent to the model):
6+
# narrow -> faithful answer needs a field or two (filtering should help)
7+
# full -> faithful answer needs rich fields/bodies (filtering shouldn't help)
8+
# neutral -> ambiguous; the model decides
9+
10+
# --- search_code ---
11+
narrow Search {repo} for where the symbol 'NewCommand' is defined and list just the file paths.
12+
full Search the code in {repo} for 'http.Client' usages and explain how the HTTP client is configured.
13+
14+
# --- get_file_contents ---
15+
narrow List the names of the files and subdirectories in the top-level directory of {repo}.
16+
full Read the README in {repo} and summarize what the project does.
17+
18+
# --- search_users ---
19+
narrow Search GitHub for users named 'octocat' and give me just their usernames.
20+
neutral Find users who contribute to {org} and tell me what you can about them.

pkg/github/__toolsnaps__/get_file_contents.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
"description": "Get the contents of a file or directory from a GitHub repository",
77
"inputSchema": {
88
"properties": {
9+
"fields": {
10+
"description": "Subset of fields to return for each entry when the path is a directory. If omitted, all fields are returned. Ignored when the path is a single file. Use this to reduce response size when listing directories and you only need specific fields, e.g. just 'name' and 'type'.",
11+
"items": {
12+
"enum": [
13+
"type",
14+
"name",
15+
"path",
16+
"size",
17+
"sha",
18+
"url",
19+
"git_url",
20+
"html_url",
21+
"download_url"
22+
],
23+
"type": "string"
24+
},
25+
"type": "array"
26+
},
927
"owner": {
1028
"description": "Repository owner (username or organization)",
1129
"type": "string"

pkg/github/__toolsnaps__/search_code.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.",
77
"inputSchema": {
88
"properties": {
9+
"fields": {
10+
"description": "Subset of fields to return for each code search result. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields; omitting 'repository' in particular drops the largest per-result object.",
11+
"items": {
12+
"enum": [
13+
"name",
14+
"path",
15+
"sha",
16+
"html_url",
17+
"repository"
18+
],
19+
"type": "string"
20+
},
21+
"type": "array"
22+
},
923
"order": {
1024
"description": "Sort order for results",
1125
"enum": [

pkg/github/__toolsnaps__/search_orgs.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"description": "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams.",
77
"inputSchema": {
88
"properties": {
9+
"fields": {
10+
"description": "Subset of fields to return for each organization. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields, e.g. just 'login'.",
11+
"items": {
12+
"enum": [
13+
"login",
14+
"id",
15+
"profile_url",
16+
"avatar_url",
17+
"details"
18+
],
19+
"type": "string"
20+
},
21+
"type": "array"
22+
},
923
"order": {
1024
"description": "Sort order",
1125
"enum": [

pkg/github/__toolsnaps__/search_users.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.",
77
"inputSchema": {
88
"properties": {
9+
"fields": {
10+
"description": "Subset of fields to return for each user. If omitted, all fields are returned. Use this to reduce response size when you only need specific fields, e.g. just 'login'.",
11+
"items": {
12+
"enum": [
13+
"login",
14+
"id",
15+
"profile_url",
16+
"avatar_url",
17+
"details"
18+
],
19+
"type": "string"
20+
},
21+
"type": "array"
22+
},
923
"order": {
1024
"description": "Sort order",
1125
"enum": [

pkg/github/minimal_types.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,22 @@ var tagFieldEnum = []any{"name", "sha"}
938938
// matching the JSON field names of MinimalRelease.
939939
var releaseFieldEnum = []any{"id", "tag_name", "name", "body", "html_url", "published_at", "prerelease", "draft", "author"}
940940

941+
// codeSearchItemFieldEnum lists the selectable fields for search_code result
942+
// items, matching the JSON field names of MinimalCodeSearchItem. The nested
943+
// repository object is the heaviest field, so omitting it is the main lever for
944+
// shrinking large result sets.
945+
var codeSearchItemFieldEnum = []any{"name", "path", "sha", "html_url", "repository"}
946+
947+
// userSearchFieldEnum lists the selectable fields for search_users and
948+
// search_orgs result items, matching the JSON field names of MinimalUser.
949+
var userSearchFieldEnum = []any{"login", "id", "profile_url", "avatar_url", "details"}
950+
951+
// fileContentFieldEnum lists the selectable fields for get_file_contents
952+
// directory listings, matching the JSON field names of
953+
// github.RepositoryContent that appear for directory entries. Only applied when
954+
// the requested path is a directory; ignored for single files.
955+
var fileContentFieldEnum = []any{"type", "name", "path", "size", "sha", "url", "git_url", "html_url", "download_url"}
956+
941957
// filterFields marshals v to a JSON object and returns a map containing only the
942958
// requested fields. Fields that are unknown or absent from the JSON (for example
943959
// empty values dropped via omitempty) are skipped.

pkg/github/repositories.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,14 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
762762
Type: "string",
763763
Description: "Accepts optional commit SHA. If specified, it will be used instead of ref",
764764
},
765+
"fields": {
766+
Type: "array",
767+
Description: "Subset of fields to return for each entry when the path is a directory. If omitted, all fields are returned. Ignored when the path is a single file. Use this to reduce response size when listing directories and you only need specific fields, e.g. just 'name' and 'type'.",
768+
Items: &jsonschema.Schema{
769+
Type: "string",
770+
Enum: fileContentFieldEnum,
771+
},
772+
},
765773
},
766774
Required: []string{"owner", "repo"},
767775
},
@@ -794,6 +802,11 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
794802
return utils.NewToolResultError(err.Error()), nil, nil
795803
}
796804

805+
fields, err := OptionalStringArrayParam(args, "fields")
806+
if err != nil {
807+
return utils.NewToolResultError(err.Error()), nil, nil
808+
}
809+
797810
client, err := deps.GetClient(ctx)
798811
if err != nil {
799812
return utils.NewToolResultError("failed to get GitHub client"), nil, nil
@@ -907,7 +920,15 @@ func GetFileContents(t translations.TranslationHelperFunc) inventory.ServerTool
907920
return utils.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)%s", fileSHA, successNote), result), nil, nil
908921
} else if dirContent != nil {
909922
// file content or file SHA is nil which means it's a directory
910-
r, err := json.Marshal(dirContent)
923+
var payload any = dirContent
924+
if len(fields) > 0 {
925+
filtered, err := filterEachField(dirContent, fields)
926+
if err != nil {
927+
return utils.NewToolResultErrorFromErr("failed to filter directory contents", err), nil, nil
928+
}
929+
payload = filtered
930+
}
931+
r, err := json.Marshal(payload)
911932
if err != nil {
912933
return utils.NewToolResultError("failed to marshal response"), nil, nil
913934
}

pkg/github/repositories_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,68 @@ func Test_GetFileContents(t *testing.T) {
477477
}
478478
}
479479

480+
func Test_GetFileContents_DirectoryFieldFiltering(t *testing.T) {
481+
mockDirContent := []*github.RepositoryContent{
482+
{
483+
Type: github.Ptr("file"),
484+
Name: github.Ptr("README.md"),
485+
Path: github.Ptr("README.md"),
486+
SHA: github.Ptr("abc123"),
487+
Size: github.Ptr(42),
488+
HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"),
489+
DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"),
490+
},
491+
{
492+
Type: github.Ptr("dir"),
493+
Name: github.Ptr("src"),
494+
Path: github.Ptr("src"),
495+
SHA: github.Ptr("def456"),
496+
HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"),
497+
},
498+
}
499+
500+
serverTool := GetFileContents(translations.NullTranslationHelper)
501+
client := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
502+
GetReposByOwnerByRepo: mockResponse(t, http.StatusOK, "{\"name\": \"repo\", \"default_branch\": \"main\"}"),
503+
GetReposGitRefByOwnerByRepoByRef: mockResponse(t, http.StatusOK, "{\"ref\": \"refs/heads/main\", \"object\": {\"sha\": \"\"}}"),
504+
GetReposContentsByOwnerByRepoByPath: expectQueryParams(t, map[string]string{}).andThen(
505+
mockResponse(t, http.StatusOK, mockDirContent),
506+
),
507+
GetRawReposContentsByOwnerByRepoByPath: expectQueryParams(t, map[string]string{"branch": "main"}).andThen(
508+
mockResponse(t, http.StatusNotFound, nil),
509+
),
510+
}))
511+
deps := BaseDeps{Client: client}
512+
handler := serverTool.Handler(deps)
513+
514+
request := createMCPRequest(map[string]any{
515+
"owner": "owner",
516+
"repo": "repo",
517+
"path": "src/",
518+
"fields": []any{"name", "type"},
519+
})
520+
521+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
522+
require.NoError(t, err)
523+
require.False(t, result.IsError)
524+
525+
textContent := getTextResult(t, result)
526+
527+
// Each directory entry is reduced to the requested fields only.
528+
var returned []map[string]any
529+
require.NoError(t, json.Unmarshal([]byte(textContent.Text), &returned))
530+
require.Len(t, returned, 2)
531+
for _, entry := range returned {
532+
require.Len(t, entry, 2)
533+
assert.Contains(t, entry, "name")
534+
assert.Contains(t, entry, "type")
535+
}
536+
537+
assert.NotContains(t, textContent.Text, "download_url")
538+
assert.NotContains(t, textContent.Text, "html_url")
539+
assert.NotContains(t, textContent.Text, "sha")
540+
}
541+
480542
func Test_ForkRepository(t *testing.T) {
481543
// Verify tool definition once
482544
serverTool := ForkRepository(translations.NullTranslationHelper)

0 commit comments

Comments
 (0)