Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ The following sets of tools are available:
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/repo-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/repo-light.png"><img src="pkg/octicons/icons/repo-light.png" width="20" height="20" alt="repo"></picture> | `repos` | GitHub Repository related tools |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/shield-lock-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/shield-lock-light.png"><img src="pkg/octicons/icons/shield-lock-light.png" width="20" height="20" alt="shield-lock"></picture> | `secret_protection` | Secret protection related tools, such as GitHub Secret Scanning |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/shield-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/shield-light.png"><img src="pkg/octicons/icons/shield-light.png" width="20" height="20" alt="shield"></picture> | `security_advisories` | Security advisories related tools |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/beaker-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/beaker-light.png"><img src="pkg/octicons/icons/beaker-light.png" width="20" height="20" alt="beaker"></picture> | `skills` | Agent Skills discovery via skill:// resources from arbitrary GitHub repositories (experimental, see agentskills.io) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/star-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/star-light.png"><img src="pkg/octicons/icons/star-light.png" width="20" height="20" alt="star"></picture> | `stargazers` | GitHub Stargazers related tools |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/people-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/people-light.png"><img src="pkg/octicons/icons/people-light.png" width="20" height="20" alt="people"></picture> | `users` | GitHub User related tools |
<!-- END AUTOMATED TOOLSETS -->
Expand Down Expand Up @@ -1490,6 +1491,17 @@ The following sets of tools are available:

<details>

<summary><picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/beaker-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/beaker-light.png"><img src="pkg/octicons/icons/beaker-light.png" width="20" height="20" alt="beaker"></picture> Skills</summary>

- **list_repo_skills** - List Agent Skills in a repository
- **Required OAuth Scopes**: `repo`
- `owner`: Repository owner (username or organization name). (string, required)
- `repo`: Repository name. (string, required)

</details>

<details>

<summary><picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/star-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/star-light.png"><img src="pkg/octicons/icons/star-light.png" width="20" height="20" alt="star"></picture> Stargazers</summary>

- **list_starred_repositories** - List starred repositories
Expand Down
1 change: 1 addition & 0 deletions docs/remote-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/repo-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/repo-light.png"><img src="../pkg/octicons/icons/repo-light.png" width="20" height="20" alt="repo"></picture><br>`repos` | GitHub Repository related tools | https://api.githubcopilot.com/mcp/x/repos | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/repos/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/shield-lock-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/shield-lock-light.png"><img src="../pkg/octicons/icons/shield-lock-light.png" width="20" height="20" alt="shield-lock"></picture><br>`secret_protection` | Secret protection related tools, such as GitHub Secret Scanning | https://api.githubcopilot.com/mcp/x/secret_protection | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/secret_protection/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/shield-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/shield-light.png"><img src="../pkg/octicons/icons/shield-light.png" width="20" height="20" alt="shield"></picture><br>`security_advisories` | Security advisories related tools | https://api.githubcopilot.com/mcp/x/security_advisories | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/security_advisories/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/beaker-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/beaker-light.png"><img src="../pkg/octicons/icons/beaker-light.png" width="20" height="20" alt="beaker"></picture><br>`skills` | Agent Skills discovery via skill:// resources from arbitrary GitHub repositories (experimental, see agentskills.io) | https://api.githubcopilot.com/mcp/x/skills | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-skills&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fskills%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/skills/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-skills&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fskills%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/star-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/star-light.png"><img src="../pkg/octicons/icons/star-light.png" width="20" height="20" alt="star"></picture><br>`stargazers` | GitHub Stargazers related tools | https://api.githubcopilot.com/mcp/x/stargazers | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-stargazers&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fstargazers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/stargazers/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-stargazers&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fstargazers%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/people-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/people-light.png"><img src="../pkg/octicons/icons/people-light.png" width="20" height="20" alt="people"></picture><br>`users` | GitHub User related tools | https://api.githubcopilot.com/mcp/x/users | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/users/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%2Freadonly%22%7D) |
<!-- END AUTOMATED TOOLSETS -->
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/yosida95/uritemplate/v3 v3.0.2
golang.org/x/oauth2 v0.36.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -46,5 +47,4 @@ require (
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
26 changes: 26 additions & 0 deletions pkg/github/__toolsnaps__/list_repo_skills.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"annotations": {
"idempotentHint": false,
"readOnlyHint": true,
"title": "List Agent Skills in a repository"
},
"description": "List Agent Skills (SKILL.md files) defined in a GitHub repository. Returns each discovered skill's name plus a `skill://` URI you can pass directly to `resources/read` to fetch its SKILL.md. Recognizes the agentskills.io directory conventions: skills/*/SKILL.md, skills/{namespace}/*/SKILL.md, plugins/*/skills/*/SKILL.md, and root-level */SKILL.md. Use this when you need to discover what skills a repository exposes before reading any of them.",
"inputSchema": {
"properties": {
"owner": {
"description": "Repository owner (username or organization name).",
"type": "string"
},
"repo": {
"description": "Repository name.",
"type": "string"
}
},
"required": [
"owner",
"repo"
],
"type": "object"
},
"name": "list_repo_skills"
}
4 changes: 4 additions & 0 deletions pkg/github/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ func AllResources(t translations.TranslationHelperFunc) []inventory.ServerResour
GetRepositoryResourceCommitContent(t),
GetRepositoryResourceTagContent(t),
GetRepositoryResourcePrContent(t),

// Skill resources (SEP-2640): per-file template for any skill in any
// GitHub repo. Gated on the `skills` toolset.
GetSkillResourceFile(t),
}
}
30 changes: 30 additions & 0 deletions pkg/github/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"slices"
"strings"
"time"

Expand All @@ -13,6 +14,7 @@ import (
"github.com/github/github-mcp-server/pkg/octicons"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/github/github-mcp-server/skills"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

Expand Down Expand Up @@ -108,6 +110,22 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
o(serverOpts)
}

// Declare the io.modelcontextprotocol/skills extension (SEP-2640) for the
// bundled Agent Skills. Must happen before NewServer since capabilities
// are captured at construction.
skillsPublisher := &skills.Publisher{Registry: skills.Bundled()}
skillsPublisher.DeclareCapability(serverOpts)

// When the `skills` toolset is enabled, skills/get and
// resources/directory/read additionally answer for repo-hosted skills —
// the SEP's unenumerable-catalog case, reachable by URI only.
if slices.ContainsFunc(inv.EnabledToolsets(), func(ts inventory.ToolsetMetadata) bool {
return ts.ID == ToolsetMetadataSkills.ID
}) {
skillsPublisher.DynamicGet = RepoSkillEntry
skillsPublisher.DynamicDirectoryRead = RepoSkillDirectory
}

ghServer := NewServer(cfg.Version, cfg.Translator("SERVER_NAME", "github-mcp-server"), cfg.Translator("SERVER_TITLE", "GitHub MCP Server"), serverOpts)

// Add middlewares. Order matters - for example, the error context middleware should be applied last so that it runs FIRST (closest to the handler) to ensure all errors are captured,
Expand All @@ -123,6 +141,15 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
// Register GitHub tools/resources/prompts from the inventory.
inv.RegisterAll(ctx, ghServer, deps, cfg.ToolHandlerMiddleware...)

// Register the bundled Agent Skills: each skill file as a skill://
// resource, plus the extension's skills/list, skills/get, and
// resources/directory/read methods. Lives here — not in the stdio
// bootstrap — so the HTTP transport, which builds an mcp.Server per
// request through this same constructor, serves them too.
if err := skillsPublisher.Install(ghServer); err != nil {
return nil, fmt.Errorf("failed to install bundled skills: %w", err)
}

// Register MCP App UI resources whenever the embedded UI assets are
// available. The resources are static HTML and are only referenced by
// tools when the remote_mcp_ui_apps feature flag is enabled for the
Expand Down Expand Up @@ -199,6 +226,9 @@ func CompletionsHandler(getClient GetClientFn) func(ctx context.Context, req *mc
if strings.HasPrefix(req.Params.Ref.URI, "repo://") {
return RepositoryResourceCompletionHandler(getClient)(ctx, req)
}
if strings.HasPrefix(req.Params.Ref.URI, "skill://") {
return SkillResourceCompletionHandler(getClient)(ctx, req)
}
return nil, fmt.Errorf("unsupported resource URI: %s", req.Params.Ref.URI)
case "ref/prompt":
return nil, nil
Expand Down
196 changes: 196 additions & 0 deletions pkg/github/skills_dynamic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package github

import (
"context"
"fmt"
"path"
"slices"
"strings"

"github.com/github/github-mcp-server/skills"
gogithub "github.com/google/go-github/v89/github"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

// This file backs the skills extension's Dynamic* hooks for repo-hosted
// skills. Repo-hosted skills are the SEP's "unenumerable catalog" case:
// they never appear in skills/list (any GitHub repository may hold one),
// but a host holding a skill://{owner}/{repo}/{skill_name}/SKILL.md URI —
// from list_repo_skills, server instructions, or the user — can call
// skills/get on it and receive the same digested, content-bindable entry a
// listing would have carried.

// RepoSkillEntry resolves a repo-hosted skill URI to its SEP-2640 entry:
// frontmatter parsed from the skill's SKILL.md and a complete resources set
// with the digest of every file in the skill directory. It returns (nil, nil)
// when the URI does not identify a repo-hosted skill, letting the caller
// answer -32602.
func RepoSkillEntry(ctx context.Context, uri string) (*skills.Entry, error) {
owner, repo, skillName, filePath, err := parseSkillFileURI(skillResourceFileURITemplate, uri)
if err != nil || filePath != skills.SkillFile {
// Not shaped like a repo-hosted skill's SKILL.md URI.
return nil, nil //nolint:nilerr // absence, not failure — caller answers -32602
}

deps := MustDepsFromContext(ctx)
client, err := deps.GetClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

dir, entries, err := repoSkillTree(ctx, client, owner, repo, skillName)
if err != nil {
return nil, err
}
if dir == "" {
return nil, nil // skill not found in the repository
}

// Fetch every file in the skill directory and digest its raw bytes.
// Skills are small by design (a SKILL.md plus a handful of supporting
// files), so one blob fetch per file is acceptable for skills/get,
// which hosts call at approval/refresh time rather than per read.
var files []skills.File
for _, entry := range entries {
if entry.GetType() != "blob" || !strings.HasPrefix(entry.GetPath(), dir+"/") {
continue
}
content, _, err := client.Git.GetBlobRaw(ctx, owner, repo, entry.GetSHA())
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", entry.GetPath(), err)
}
files = append(files, skills.File{
Path: strings.TrimPrefix(entry.GetPath(), dir+"/"),
Content: content,
})
}

s, err := skills.New(path.Join(owner, repo, skillName), files)
if err != nil {
// The directory exists but does not form a conforming skill (bad or
// missing frontmatter, name mismatch). The server cannot serve it as
// a skill; surface why under the SEP's -32602 contract.
return nil, &jsonrpc.Error{
Code: jsonrpc.CodeInvalidParams,
Message: fmt.Sprintf("not a conforming skill: %v", err),
}
}
entry := s.Entry()
return &entry, nil
}

// RepoSkillDirectory lists the direct children of a directory resource inside
// a repo-hosted skill namespace: skill://{owner}/{repo} enumerates the
// repository's discovered skills as directories, and deeper URIs walk a
// skill's own tree. It returns (nil, nil) when the URI is not a directory
// this server serves, letting the caller answer -32602.
func RepoSkillDirectory(ctx context.Context, uri string) ([]*mcp.Resource, error) {
trimmed, ok := strings.CutPrefix(uri, "skill://")
if !ok || strings.Contains(trimmed, "..") {
return nil, nil
}
segs := strings.Split(trimmed, "/")
if len(segs) < 2 || segs[0] == "" || segs[1] == "" {
return nil, nil
}
owner, repo := segs[0], segs[1]

deps := MustDepsFromContext(ctx)
client, err := deps.GetClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

// skill://{owner}/{repo}: the repository's skill namespace root.
if len(segs) == 2 {
names, err := discoverSkills(ctx, client, owner, repo)
if err != nil {
return nil, err
}
children := make([]*mcp.Resource, 0, len(names))
for _, name := range names {
children = append(children, &mcp.Resource{
URI: "skill://" + owner + "/" + repo + "/" + name,
Name: name,
MIMEType: skills.DirectoryMIMEType,
})
}
return children, nil
}

skillName := segs[2]
subPath := strings.Join(segs[3:], "/")

dir, entries, err := repoSkillTree(ctx, client, owner, repo, skillName)
if err != nil {
return nil, err
}
if dir == "" {
return nil, nil
}

target := dir
if subPath != "" {
target = dir + "/" + subPath
}

var children []*mcp.Resource
targetExists := subPath == "" // the skill root exists by discovery
for _, entry := range entries {
entryPath := entry.GetPath()
if entryPath == target && entry.GetType() == "tree" {
targetExists = true
}
if entryPath == target && entry.GetType() == "blob" {
// A file, not a directory — not listable.
return nil, nil
}
rel, ok := strings.CutPrefix(entryPath, target+"/")
if !ok || strings.Contains(rel, "/") {
continue // not a direct child
}
childRel := rel
if subPath != "" {
childRel = subPath + "/" + rel
}
switch entry.GetType() {
case "tree":
children = append(children, &mcp.Resource{
URI: SkillFileURI(owner, repo, skillName, childRel),
Name: rel,
MIMEType: skills.DirectoryMIMEType,
})
case "blob":
children = append(children, &mcp.Resource{
URI: SkillFileURI(owner, repo, skillName, childRel),
Name: rel,
MIMEType: skills.FileMIMEType(rel),
})
}
}
if !targetExists {
return nil, nil
}
slices.SortFunc(children, func(a, b *mcp.Resource) int { return strings.Compare(a.URI, b.URI) })
return children, nil
}

// repoSkillTree fetches the repository tree and locates the named skill's
// directory. It returns the skill directory (empty when the skill is not
// found) alongside the full tree entries for further filtering.
func repoSkillTree(ctx context.Context, client *gogithub.Client, owner, repo, skillName string) (string, []*gogithub.TreeEntry, error) {
tree, _, err := client.Git.GetTree(ctx, owner, repo, "HEAD", true)
if err != nil {
return "", nil, fmt.Errorf("failed to get repository tree: %w", err)
}
for _, entry := range tree.Entries {
if entry.GetType() != "blob" {
continue
}
if skill := matchSkillConventions(entry.GetPath()); skill != nil && skill.Name == skillName {
return skill.Dir, tree.Entries, nil
}
}
return "", tree.Entries, nil
}
Loading